Compare commits

...

53 Commits

Author SHA1 Message Date
Shadowfacts 9b30b48016 Bump build number and update changelog 2021-06-26 18:28:38 -04:00
Shadowfacts bd49683e13 Fix not being able to select assets on iOS 15 beta 2 2021-06-26 17:18:04 -04:00
Shadowfacts c22945b1e7 Use sheetPresentationController property 2021-06-26 17:02:17 -04:00
Shadowfacts 0a16a2e261 Fix potential data races 2021-06-26 16:51:54 -04:00
Shadowfacts b95819cada Fix crash when switching accounts 2021-06-26 16:42:56 -04:00
Shadowfacts dc1ea1bed9 Fix timeline momentum scrolling stopping due to adding footer section 2021-06-26 15:54:10 -04:00
Shadowfacts 5f9fe505d5 Add pref to disable infinite scrolling on timelines
Closes #125
2021-06-25 23:28:43 -04:00
Shadowfacts 5b8e97287e Convert TimelineTableViewController to use DiffableTimelineLikeTableViewController 2021-06-20 22:27:38 -04:00
Shadowfacts 49572c1fec Add DiffableTimelineLikeTableViewController 2021-06-20 22:27:29 -04:00
Shadowfacts ebb0770198 Add context menu action to remove attachments in Compose 2021-06-18 11:32:17 -04:00
Shadowfacts 27e05cc72d Enable focus loop debugging in debug 2021-06-12 22:17:59 -04:00
Shadowfacts 4ca48a5f50 Add iOS 15 compilation condition 2021-06-12 22:17:41 -04:00
Shadowfacts 230bd50661 Disable selection of presenting sidebar items on focus 2021-06-12 22:17:09 -04:00
Shadowfacts 4f2f8d517f Don't initiate table view cell drag while user is selecting poll options 2021-06-12 19:22:51 -04:00
Shadowfacts 130da9d4cc Improve status collapse animation
Use an additional label with no content and no height to absorb the
extra space creating during collapse when the content text view
disappears immediately.
2021-06-12 11:39:15 -04:00
Shadowfacts 472b9aa5e2 Fixes for large image animations on devices with square screns 2021-06-12 11:26:44 -04:00
Shadowfacts 3413dff8f9 Present compose screen in new window on iOS 15 and iPad/Mac 2021-06-11 10:50:31 -04:00
Shadowfacts 66e8fce488 Fix crash when conversation VC tries to restore from unloaded status 2021-06-11 10:19:59 -04:00
Shadowfacts aa2d333f4a Disable transparent nav bar on page view controllers 2021-06-10 10:55:09 -04:00
Shadowfacts c8a45d8eef Add Open in New Window menu item to profiles 2021-06-10 10:52:27 -04:00
Shadowfacts 40f5be28f6 Cleanup un/follow menu action 2021-06-10 10:36:02 -04:00
Shadowfacts 7c9287543c Fix crash due to PencilKit undo manager not being available until viewDidAppear 2021-06-10 10:33:24 -04:00
Shadowfacts 2a05b6d326 Add pointer hover effects to compose poll buttons 2021-06-09 19:18:54 -04:00
Shadowfacts 2499d25432 Use built-in sheet for asset picker on iOS 15 2021-06-09 19:12:10 -04:00
Shadowfacts 9417872790 Don't show Reply action in menu button on statuses 2021-06-09 17:10:44 -04:00
Shadowfacts c02a1bbf74 Make Pin status action title clearer 2021-06-09 17:10:13 -04:00
Shadowfacts 0a894b219a Allow Open in New Window action on iPadOS 2021-06-09 17:09:59 -04:00
Shadowfacts 22803668d2 Remove ellipsis from Share menu item title 2021-06-09 17:09:45 -04:00
Shadowfacts 2f6d1cb069 Use plain list style for Compose attachments 2021-06-09 17:08:59 -04:00
Shadowfacts 8889261b6b Fix compose reply avatar scroll effect not working on iOS 15 2021-06-09 11:01:11 -04:00
Shadowfacts 91f1a5195c Use visibility bar button item selection state instead of changing icon 2021-06-08 15:00:48 -04:00
Shadowfacts 1a5b958b1a Hide compose progress bar while there is no progress
On iOS 15, the progress bar displays a little bit of progress even at 0
2021-06-08 14:54:42 -04:00
Shadowfacts d667f6362c Use UniformTypeIdentifiers framework for everything 2021-06-07 20:08:46 -04:00
Shadowfacts ef1db466b9
Fix VoiceOver reading profile field names/values in incorrect order 2021-06-06 22:35:15 -04:00
Shadowfacts 0566f0ddfa
Fix More button in profile header not being VoiceOver accessible 2021-06-06 22:35:03 -04:00
Shadowfacts f54d4d757f
Make status attachments VoiceOver accessible 2021-06-06 22:31:11 -04:00
Shadowfacts fbc5d6eed9
Make timeline status cells single accessibility elements 2021-06-06 22:16:44 -04:00
Shadowfacts 2c4d2ce551
Make polls in statuses accessible 2021-06-06 22:11:29 -04:00
Shadowfacts bbe260bc9e
Construct PKToolPicker ourselves 2021-06-06 21:33:17 -04:00
Shadowfacts 2fe19a5abe
Add fast account switching indicator to tab bar item 2021-06-06 18:30:46 -04:00
Shadowfacts feacf576d7
Allow draging accounts in Preferences into new scenes 2021-06-06 14:55:18 -04:00
Shadowfacts ceb58f1d92
Add state restoration for current account in main scene 2021-06-06 14:55:04 -04:00
Shadowfacts 806591f5b7
Remove old framework from Xcode project 2021-05-24 19:30:20 -04:00
Shadowfacts 18ce21c2c6
Add Open in Tusker action extension 2021-05-24 19:30:11 -04:00
Shadowfacts 47fb0ea868
Update PLCrashReporter 2021-05-22 13:45:18 -04:00
Shadowfacts ffe6450b26
Xcode recommendations, use AnyObject instead of class in protocol requirements 2021-05-22 13:44:58 -04:00
Shadowfacts b51c1c03cb
Fix poll option percentages getting cut off
Closes #120
2021-05-22 11:44:50 -04:00
Shadowfacts e745d78d67
Fix polls not being collapsed inside CW
Closes #119
2021-05-22 11:30:56 -04:00
Shadowfacts 4c9d5e8465
Fix nav bar on iPad search screen hiding 2021-05-22 11:25:12 -04:00
Shadowfacts 9ec7177bfa
Fix crash when searching fails 2021-05-22 11:22:01 -04:00
Shadowfacts 421881d461
Remove dead code 2021-05-13 22:42:26 -04:00
Shadowfacts c78f152670
Animate attachment rows in when picking assets 2021-05-13 22:34:26 -04:00
Shadowfacts dabcae0905
Fix being unable to commit previewed profile from timeline status 2021-05-13 22:25:28 -04:00
81 changed files with 1961 additions and 375 deletions

View File

@ -1,5 +1,40 @@
# Changelog # Changelog
## 2021.1 (20)
This is a big one! In addition to a bunch of fixes for anyone on the iOS 15 beta, there are a couple of big ticket features, including the Open in Tusker action extension and the Disable Infinite Scrolling preference.
Features/Improvements:
- Add Open in Tusker action extension
- Quickly search for any URL in Tusker
- In a share sheet, scroll to the bottom, tap "Edit Actions..." and turn on the "Open in Tusker" action
- Add Digital Wellness preference to disable infinite scrolling
- Add fast account switching indicator to My Profile tab
- Improve VoiceOver accessibility of polls and timeline statuses
- iPadOS: Create multiple main windows for different accounts by dragging from an account in Preferences
- iPadOS: Delete attachments on Compose screen by right-clicking and selecting Delete
- iPadOS 15: Add Open in New Window context menu action to most things
- iPadOS 15: Allow dragging the Compose sheet into a separate window
Bugfixes:
- Fix being unable to commit previewed account from timeline status
- Fix crash when searching fails
- Fix poll option percentages being cut off
- Fix polls not collapsing inside CWs
- Fix More button on profiles not being accessible with VoiceOver
- Fix VoiceOver reading profile fields in incorrect order
- Fix gallery animations jittering on devices with square screens (iPads, non-notched iPhones)
- Fix CW text jumping around post collapse animation
- iOS 15: Fix crash due when showing Draw Something screen in Compose
- iPadOS 14/iOS 15: Fix navigation bar turning transparent after opening the attachment gallery
- iPadOS 14/iOS 15: Fix drag-selecting poll options initiating a status cell drag interaction
- iPadOS: Fix crash when loading a previously-opened conversation window
- iPadOS 15: Fix showing Compose screen when keyboard focus moves through the sidebar
Known Issues:
- Disable Infinite Scrolling preference only affects timelines, not notifications or profiles
- iPadOS 15: The Compose sheet cannot be dismissed by swiping down
- iPadOS 15: Keyboard focus is stuck in the sidebar
## 2021.1 (19) ## 2021.1 (19)
This is an emergency fix for Tusker breaking when connecting to Mastodon instances on 3.4.0rc1. This is an emergency fix for Tusker breaking when connecting to Mastodon instances on 3.4.0rc1.

29
OpenInTusker/Action.js Normal file
View File

@ -0,0 +1,29 @@
//
// Action.js
// OpenInTusker
//
// Created by Shadowfacts on 5/22/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
var Action = function() {};
Action.prototype = {
run: function(arguments) {
const results = {
url: window.location.href,
};
const el = document.querySelector('link[rel=alternate][type="application/activity+json"]');
if (el) {
results.activityPubURL = el.href;
}
arguments.completionFunction(results);
},
finalize: function(arguments) {
}
};
var ExtensionPreprocessingJS = new Action();

View File

@ -0,0 +1,100 @@
//
// ActionViewController.swift
// OpenInTusker
//
// Created by Shadowfacts on 5/23/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import MobileCoreServices
class ActionViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
findURLFromWebPage { (components) in
if let components = components {
self.searchForURLInApp(components)
} else {
self.findURLItem { (components) in
if let components = components {
self.searchForURLInApp(components)
}
}
}
}
}
private func findURLFromWebPage(completion: @escaping (URLComponents?) -> Void) {
for item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) else {
continue
}
provider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil) { (result, error) in
guard let result = result as? [String: Any],
let jsResult = result[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any],
let urlString = jsResult["activityPubURL"] as? String ?? jsResult["url"] as? String,
let components = URLComponents(string: urlString) else {
completion(nil)
return
}
completion(components)
}
return
}
}
completion(nil)
}
private func findURLItem(completion: @escaping (URLComponents?) -> Void) {
for item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) else {
continue
}
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { (result, error) in
guard let result = result as? URL,
let components = URLComponents(url: result, resolvingAgainstBaseURL: false) else {
completion(nil)
return
}
completion(components)
}
return
}
}
completion(nil)
}
private func searchForURLInApp(_ components: URLComponents) {
var components = components
components.scheme = "tusker"
self.openURL(components.url!)
self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
}
@objc private func openURL(_ url: URL) {
var responder: UIResponder = self
while let parent = responder.next {
if let application = parent as? UIApplication {
application.perform(#selector(openURL(_:)), with: url)
break
} else {
responder = parent
}
}
}
@IBAction func done() {
extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
}
}

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ObA-dk-sSI">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Image-->
<scene sceneID="7MM-of-jgj">
<objects>
<viewController title="Image" id="ObA-dk-sSI" customClass="ActionViewController" customModule="OpenInTusker" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="zMn-AG-sqS">
<rect key="frame" x="0.0" y="0.0" width="320" height="528"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar contentMode="scaleToFill" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="751" translatesAutoresizingMaskIntoConstraints="NO" id="NOA-Dm-cuz">
<rect key="frame" x="0.0" y="44" width="320" height="44"/>
<items>
<navigationItem id="3HJ-uW-3hn">
<barButtonItem key="leftBarButtonItem" title="Done" style="done" id="WYi-yp-eM6">
<connections>
<action selector="done" destination="ObA-dk-sSI" id="Qdu-qn-U6V"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Unable to find Mastodon link on this page." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Yho-gp-VyR">
<rect key="frame" x="0.0" y="254" width="320" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="VVe-Uw-JpX"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="VVe-Uw-JpX" firstAttribute="trailing" secondItem="NOA-Dm-cuz" secondAttribute="trailing" id="A05-Pj-hrr"/>
<constraint firstItem="NOA-Dm-cuz" firstAttribute="leading" secondItem="VVe-Uw-JpX" secondAttribute="leading" id="HxO-8t-aoh"/>
<constraint firstItem="Yho-gp-VyR" firstAttribute="centerY" secondItem="zMn-AG-sqS" secondAttribute="centerY" id="R7q-OB-hhA"/>
<constraint firstItem="Yho-gp-VyR" firstAttribute="leading" secondItem="VVe-Uw-JpX" secondAttribute="leading" id="TEy-zi-dP7"/>
<constraint firstItem="Yho-gp-VyR" firstAttribute="trailing" secondItem="VVe-Uw-JpX" secondAttribute="trailing" id="Uvn-0x-Y6N"/>
<constraint firstItem="NOA-Dm-cuz" firstAttribute="top" secondItem="VVe-Uw-JpX" secondAttribute="top" id="we0-1t-bgp"/>
</constraints>
</view>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="320" height="528"/>
<connections>
<outlet property="view" destination="zMn-AG-sqS" id="Qma-de-2ek"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="X47-rx-isc" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-61" y="-57"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

55
OpenInTusker/Info.plist Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Open in Tusker</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>Action</string>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
<true/>
<key>NSExtensionServiceAllowsTouchBarItem</key>
<true/>
<key>NSExtensionServiceFinderPreviewIconName</key>
<string>NSActionTemplate</string>
<key>NSExtensionServiceTouchBarBezelColorName</key>
<string>TouchBarBezel</string>
<key>NSExtensionServiceTouchBarIconName</key>
<string>NSActionTemplate</string>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.ui-services</string>
</dict>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,103 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "60x60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "60x60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "76x76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "83.5x83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024x1024@1x.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,14 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "mac",
"color" : {
"reference" : "systemPurpleColor"
}
}
]
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -155,6 +155,7 @@
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; }; D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; }; D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */; };
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; }; D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; }; D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; }; D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
@ -186,6 +187,7 @@
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; }; D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; }; D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; }; D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */; };
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; }; D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; }; D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; }; D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
@ -303,10 +305,17 @@
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; }; D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.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 */; }; D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */; };
D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */; };
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; }; D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; }; D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; }; D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; };
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; }; D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; };
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; }; D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; }; D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
@ -363,9 +372,27 @@
remoteGlobalIDString = D6D4DDCB212518A000E1C4BB; remoteGlobalIDString = D6D4DDCB212518A000E1C4BB;
remoteInfo = Tusker; remoteInfo = Tusker;
}; };
D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
proxyType = 1;
remoteGlobalIDString = D6E343A7265AAD6B00C4AA01;
remoteInfo = OpenInTusker;
};
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
D6E3438F2659849800C4AA01 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
D6F953E52125197500CF0F2B /* Embed Frameworks */ = { D6F953E52125197500CF0F2B /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -388,7 +415,6 @@
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; }; 0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; }; 04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; }; 04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
0461A38F2163CBAE00C0A807 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
04D14BAE22B34A2800642648 /* GalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewController.swift; sourceTree = "<group>"; }; 04D14BAE22B34A2800642648 /* GalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewController.swift; sourceTree = "<group>"; };
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; }; 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
@ -530,6 +556,7 @@
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; }; D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; }; D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; };
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; }; D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableTimelineLikeTableViewController.swift; sourceTree = "<group>"; };
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; }; D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; };
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -565,6 +592,7 @@
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; }; D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; }; D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; }; D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherIndicatorView.swift; sourceTree = "<group>"; };
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; }; D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; }; D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; }; D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
@ -683,10 +711,19 @@
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; }; D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; }; D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreTableViewCell.swift; sourceTree = "<group>"; };
D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfirmLoadMoreTableViewCell.xib; sourceTree = "<group>"; };
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; }; D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; }; D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; }; D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = "<group>"; };
D6E343AF265AAD6B00C4AA01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
D6E343B1265AAD6B00C4AA01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D6E343B5265AAD6B00C4AA01 /* OpenInTusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInTusker.entitlements; sourceTree = "<group>"; };
D6E343B9265AAD8C00C4AA01 /* Action.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = "<group>"; };
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAutocompleteView.swift; sourceTree = "<group>"; }; D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAutocompleteView.swift; sourceTree = "<group>"; };
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcher.swift; sourceTree = "<group>"; }; D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcher.swift; sourceTree = "<group>"; };
D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcherTests.swift; sourceTree = "<group>"; }; D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcherTests.swift; sourceTree = "<group>"; };
@ -752,6 +789,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
D6E343A5265AAD6B00C4AA01 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
@ -1413,6 +1457,7 @@
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */, D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */, D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */, D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
D67C57A721E2649B00C3118B /* Account Detail */, D67C57A721E2649B00C3118B /* Account Detail */,
D626494023C122C800612E6E /* Asset Picker */, D626494023C122C800612E6E /* Asset Picker */,
D61959D0241E842400A37B8E /* Draft Cell */, D61959D0241E842400A37B8E /* Draft Cell */,
@ -1424,6 +1469,7 @@
D6A3BC872321F78000FD64D5 /* Account Cell */, D6A3BC872321F78000FD64D5 /* Account Cell */,
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */, D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
D61AC1DA232EA43100C54D2D /* Instance Cell */, D61AC1DA232EA43100C54D2D /* Instance Cell */,
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1446,6 +1492,7 @@
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */, D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */, D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */, D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */, D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */, D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
); );
@ -1466,12 +1513,12 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6BC874421961F73006163F1 /* Gifu.framework */, D6BC874421961F73006163F1 /* Gifu.framework */,
0461A38F2163CBAE00C0A807 /* Cache.framework */,
D61099AC2144B0CC00432DC2 /* Pachyderm */, D61099AC2144B0CC00432DC2 /* Pachyderm */,
D61099B92144B0CC00432DC2 /* PachydermTests */, D61099B92144B0CC00432DC2 /* PachydermTests */,
D6D4DDCE212518A000E1C4BB /* Tusker */, D6D4DDCE212518A000E1C4BB /* Tusker */,
D6D4DDE3212518A200E1C4BB /* TuskerTests */, D6D4DDE3212518A200E1C4BB /* TuskerTests */,
D6D4DDEE212518A200E1C4BB /* TuskerUITests */, D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */,
D6D4DDCD212518A000E1C4BB /* Products */, D6D4DDCD212518A000E1C4BB /* Products */,
D65A37F221472F300087646E /* Frameworks */, D65A37F221472F300087646E /* Frameworks */,
); );
@ -1485,6 +1532,7 @@
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */, D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */,
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */, D61099AB2144B0CC00432DC2 /* Pachyderm.framework */,
D61099B32144B0CC00432DC2 /* PachydermTests.xctest */, D61099B32144B0CC00432DC2 /* PachydermTests.xctest */,
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1552,6 +1600,28 @@
path = TuskerUITests; path = TuskerUITests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */ = {
isa = PBXGroup;
children = (
D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */,
D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */,
);
path = "Confirm Load More Cell";
sourceTree = "<group>";
};
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */ = {
isa = PBXGroup;
children = (
D6E343B5265AAD6B00C4AA01 /* OpenInTusker.entitlements */,
D6E343AA265AAD6B00C4AA01 /* Media.xcassets */,
D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */,
D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */,
D6E343B1265AAD6B00C4AA01 /* Info.plist */,
D6E343B9265AAD8C00C4AA01 /* Action.js */,
);
path = OpenInTusker;
sourceTree = "<group>";
};
D6F1F84E2193B9BE00F5FE67 /* Caching */ = { D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1642,11 +1712,13 @@
D6D4DDCA212518A000E1C4BB /* Resources */, D6D4DDCA212518A000E1C4BB /* Resources */,
D6F953E52125197500CF0F2B /* Embed Frameworks */, D6F953E52125197500CF0F2B /* Embed Frameworks */,
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */, D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
D6E3438F2659849800C4AA01 /* Embed App Extensions */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
D61099BF2144B0CC00432DC2 /* PBXTargetDependency */, D61099BF2144B0CC00432DC2 /* PBXTargetDependency */,
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */,
); );
name = Tusker; name = Tusker;
packageProductDependencies = ( packageProductDependencies = (
@ -1696,14 +1768,31 @@
productReference = D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */; productReference = D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing"; productType = "com.apple.product-type.bundle.ui-testing";
}; };
D6E343A7265AAD6B00C4AA01 /* OpenInTusker */ = {
isa = PBXNativeTarget;
buildConfigurationList = D6E343B6265AAD6B00C4AA01 /* Build configuration list for PBXNativeTarget "OpenInTusker" */;
buildPhases = (
D6E343A4265AAD6B00C4AA01 /* Sources */,
D6E343A5265AAD6B00C4AA01 /* Frameworks */,
D6E343A6265AAD6B00C4AA01 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = OpenInTusker;
productName = OpenInTusker;
productReference = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
D6D4DDC4212518A000E1C4BB /* Project object */ = { D6D4DDC4212518A000E1C4BB /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1200; LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1020; LastUpgradeCheck = 1250;
ORGANIZATIONNAME = Shadowfacts; ORGANIZATIONNAME = Shadowfacts;
TargetAttributes = { TargetAttributes = {
D61099AA2144B0CC00432DC2 = { D61099AA2144B0CC00432DC2 = {
@ -1729,6 +1818,9 @@
LastSwiftMigration = 1020; LastSwiftMigration = 1020;
TestTargetID = D6D4DDCB212518A000E1C4BB; TestTargetID = D6D4DDCB212518A000E1C4BB;
}; };
D6E343A7265AAD6B00C4AA01 = {
CreatedOnToolsVersion = 12.5;
};
}; };
}; };
buildConfigurationList = D6D4DDC7212518A000E1C4BB /* Build configuration list for PBXProject "Tusker" */; buildConfigurationList = D6D4DDC7212518A000E1C4BB /* Build configuration list for PBXProject "Tusker" */;
@ -1760,6 +1852,7 @@
D6D4DDEA212518A200E1C4BB /* TuskerUITests */, D6D4DDEA212518A200E1C4BB /* TuskerUITests */,
D61099AA2144B0CC00432DC2 /* Pachyderm */, D61099AA2144B0CC00432DC2 /* Pachyderm */,
D61099B22144B0CC00432DC2 /* PachydermTests */, D61099B22144B0CC00432DC2 /* PachydermTests */,
D6E343A7265AAD6B00C4AA01 /* OpenInTusker */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -1810,6 +1903,7 @@
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */, D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */, D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */, D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -1827,6 +1921,16 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
D6E343A6265AAD6B00C4AA01 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */,
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */,
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
@ -1933,6 +2037,7 @@
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */, D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */, D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */, D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */,
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */, D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */, D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
@ -2065,6 +2170,7 @@
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */, D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */, D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */, D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */, D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */, D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
@ -2085,6 +2191,7 @@
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */, D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */, D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */, 04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
@ -2170,6 +2277,14 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
D6E343A4265AAD6B00C4AA01 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
@ -2198,6 +2313,11 @@
target = D6D4DDCB212518A000E1C4BB /* Tusker */; target = D6D4DDCB212518A000E1C4BB /* Tusker */;
targetProxy = D6D4DDEC212518A200E1C4BB /* PBXContainerItemProxy */; targetProxy = D6D4DDEC212518A200E1C4BB /* PBXContainerItemProxy */;
}; };
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */;
targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
@ -2209,6 +2329,14 @@
name = LaunchScreen.storyboard; name = LaunchScreen.storyboard;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
D6E343AF265AAD6B00C4AA01 /* Base */,
);
name = MainInterface.storyboard;
sourceTree = "<group>";
};
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */ = { D6E57FA525C26FAB00341037 /* Localizable.stringsdict */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
@ -2234,6 +2362,7 @@
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Pachyderm/Info.plist; INFOPLIST_FILE = Pachyderm/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2264,6 +2393,7 @@
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Pachyderm/Info.plist; INFOPLIST_FILE = Pachyderm/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2346,6 +2476,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -2408,6 +2539,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -2445,11 +2577,11 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements; CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19; CURRENT_PROJECT_VERSION = 20;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0; "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2461,6 +2593,10 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTS_MACCATALYST = YES; SUPPORTS_MACCATALYST = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphoneos15.0]" = "SDK_IOS_15 $(inherited)";
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphonesimulator15.0]" = "SDK_IOS_15 $(inherited)";
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=macosx12.0]" = "SDK_IOS_15 $(inherited)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6"; TARGETED_DEVICE_FAMILY = "1,2,6";
}; };
@ -2474,11 +2610,11 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements; CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19; CURRENT_PROJECT_VERSION = 20;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0; "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2576,6 +2712,60 @@
}; };
name = Release; name = Release;
}; };
D6E343B7265AAD6B00C4AA01 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2021.1;
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Debug;
};
D6E343B8265AAD6B00C4AA01 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2021.1;
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Release;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@ -2633,6 +2823,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
D6E343B6265AAD6B00C4AA01 /* Build configuration list for PBXNativeTarget "OpenInTusker" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D6E343B7265AAD6B00C4AA01 /* Debug */,
D6E343B8265AAD6B00C4AA01 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
@ -2649,7 +2848,7 @@
repositoryURL = "https://github.com/microsoft/plcrashreporter"; repositoryURL = "https://github.com/microsoft/plcrashreporter";
requirement = { requirement = {
kind = upToNextMinorVersion; kind = upToNextMinorVersion;
minimumVersion = 1.7.0; minimumVersion = 1.8.0;
}; };
}; };
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = { D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1250"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@ -29,8 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -51,8 +49,6 @@
ReferencedContainer = "container:Tusker.xcodeproj"> ReferencedContainer = "container:Tusker.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1250"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@ -88,6 +88,10 @@
argument = "-com.apple.CoreData.ConcurrencyDebug 1" argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument
argument = "-UIFocusLoopDebuggerEnabled YES"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments> </CommandLineArguments>
<EnvironmentVariables> <EnvironmentVariables>
<EnvironmentVariable <EnvironmentVariable

View File

@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/microsoft/plcrashreporter", "repositoryURL": "https://github.com/microsoft/plcrashreporter",
"state": { "state": {
"branch": null, "branch": null,
"revision": "6b7ca9a2faad6ea990ff60b0a3ee4fdf3db59150", "revision": "de6b8f9db4b2a0aa859a5507550a70548e4da936",
"version": "1.7.2" "version": "1.8.1"
} }
}, },
{ {

View File

@ -65,6 +65,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
switch type { switch type {
case .mainScene:
return "main-scene"
case .showConversation, case .showConversation,
.showTimeline, .showTimeline,
.checkNotifications, .checkNotifications,
@ -76,9 +79,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
case .newPost: case .newPost:
return "compose" return "compose"
default:
fatalError("no scene for activity type \(type)")
} }
} }
} }

View File

@ -17,21 +17,39 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
return return
} }
guard LocalData.shared.onboardingComplete else {
UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil)
return
}
let account: LocalData.UserAccountInfo let account: LocalData.UserAccountInfo
let controller: MastodonController
let draft: Draft? let draft: Draft?
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity, if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
let activityAccount = UserActivityManager.getAccount(from: activity) { if let activityAccount = UserActivityManager.getAccount(from: activity) {
account = activityAccount account = activityAccount
draft = UserActivityManager.getDraft(from: activity) } else {
// todo: this potentially changes the account for the draft, should show the same warning to user as in the drafts selection screen
account = LocalData.shared.getMostRecentAccount()!
}
controller = MastodonController.getForAccount(account)
if let activityDraft = UserActivityManager.getDraft(from: activity) {
draft = activityDraft
} else if let mentioning = activity.userInfo?["mentioning"] as? String {
draft = controller.createDraft(inReplyToID: nil, mentioningAcct: mentioning)
} else {
draft = nil
}
} else { } else {
account = LocalData.shared.getMostRecentAccount()! account = LocalData.shared.getMostRecentAccount()!
controller = MastodonController.getForAccount(account)
draft = nil draft = nil
} }
let controller = MastodonController.getForAccount(account)
session.mastodonController = controller session.mastodonController = controller
controller.getOwnAccount() controller.getOwnAccount()
controller.getOwnInstance() controller.getOwnInstance()

View File

@ -69,6 +69,7 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.bookmarks</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.bookmarks</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.main-scene</string>
</array> </array>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>

View File

@ -15,12 +15,15 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? var window: UIWindow?
private var launchActivity: NSUserActivity?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let windowScene = scene as? UIWindowScene else { return } guard let windowScene = scene as? UIWindowScene else { return }
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
launchActivity = activity
}
window = UIWindow(windowScene: windowScene) window = UIWindow(windowScene: windowScene)
if let report = AppDelegate.pendingCrashReport { if let report = AppDelegate.pendingCrashReport {
@ -28,6 +31,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
handlePendingCrashReport(report, session: session) handlePendingCrashReport(report, session: session)
} else { } else {
showAppOrOnboardingUI(session: session) showAppOrOnboardingUI(session: session)
if connectionOptions.urlContexts.count > 0 {
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
}
} }
window!.makeKeyAndVisible() window!.makeKeyAndVisible()
@ -50,20 +56,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
if url.host == "x-callback-url" { if url.host == "x-callback-url" {
_ = XCBManager.handle(url: url) _ = XCBManager.handle(url: url)
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false), } else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let tabBarController = window!.rootViewController as? MainTabBarViewController, let rootViewController = window!.rootViewController as? TuskerRootViewController {
let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController,
let exploreController = exploreNavController.viewControllers.first as? ExploreViewController {
tabBarController.select(tab: .explore)
exploreNavController.popToRootViewController(animated: false)
exploreController.loadViewIfNeeded()
exploreController.searchController.isActive = true
components.scheme = "https" components.scheme = "https"
let query = url.absoluteString let query = components.string!
exploreController.searchController.searchBar.text = query rootViewController.performSearch(query: query)
exploreController.resultsController.performSearch(query: query)
} }
} }
@ -98,6 +94,14 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
DraftsManager.save() DraftsManager.save()
} }
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
if let mastodonController = window?.windowScene?.session.mastodonController {
return UserActivityManager.mainSceneActivity(accountID: mastodonController.accountInfo!.id)
} else {
return nil
}
}
func sceneWillEnterForeground(_ scene: UIScene) { func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground. // Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background. // Use this method to undo the changes made on entering the background.
@ -130,7 +134,14 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
func showAppOrOnboardingUI(session: UISceneSession? = nil) { func showAppOrOnboardingUI(session: UISceneSession? = nil) {
let session = session ?? window!.windowScene!.session let session = session ?? window!.windowScene!.session
if LocalData.shared.onboardingComplete { if LocalData.shared.onboardingComplete {
let account = LocalData.shared.getMostRecentAccount()! let account: LocalData.UserAccountInfo
if let activity = launchActivity,
let activityAccount = UserActivityManager.getAccount(from: activity) {
account = activityAccount
} else {
account = LocalData.shared.getMostRecentAccount()!
}
if session.mastodonController == nil { if session.mastodonController == nil {
session.mastodonController = MastodonController.getForAccount(account) session.mastodonController = MastodonController.getForAccount(account)
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
import UIKit import UIKit
import MobileCoreServices import UniformTypeIdentifiers
final class CompositionAttachment: NSObject, Codable, ObservableObject { final class CompositionAttachment: NSObject, Codable, ObservableObject {
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment" static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
@ -52,10 +52,10 @@ final class CompositionAttachment: NSObject, Codable, ObservableObject {
extension CompositionAttachment: Identifiable {} extension CompositionAttachment: Identifiable {}
private let imageType = kUTTypeImage as String private let imageType = UTType.image.identifier
private let mp4Type = kUTTypeMPEG4 as String private let mp4Type = UTType.mpeg4Movie.identifier
private let quickTimeType = kUTTypeQuickTimeMovie as String private let quickTimeType = UTType.quickTimeMovie.identifier
private let dataType = kUTTypeData as String private let dataType = UTType.data.identifier
extension CompositionAttachment: NSItemProviderWriting { extension CompositionAttachment: NSItemProviderWriting {
static var writableTypeIdentifiersForItemProvider: [String] { static var writableTypeIdentifiersForItemProvider: [String] {
@ -100,11 +100,11 @@ extension CompositionAttachment: NSItemProviderReading {
return try PropertyListDecoder().decode(Self.self, from: data) return try PropertyListDecoder().decode(Self.self, from: data)
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) { } else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
return CompositionAttachment(data: .image(image)) as! Self return CompositionAttachment(data: .image(image)) as! Self
} else if typeIdentifier == mp4Type || typeIdentifier == quickTimeType { } else if let type = UTType(typeIdentifier), type == .mpeg4Movie || type == .quickTimeMovie {
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let temporaryFileName = ProcessInfo().globallyUniqueString let temporaryFileName = ProcessInfo().globallyUniqueString
let fileExt = UTTypeCopyPreferredTagWithClass(typeIdentifier as CFString, kUTTagClassFilenameExtension)! let fileExt = type.preferredFilenameExtension!
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt.takeUnretainedValue() as String) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt)
try data.write(to: temporaryFileURL) try data.write(to: temporaryFileURL)
return CompositionAttachment(data: .video(temporaryFileURL)) as! Self return CompositionAttachment(data: .video(temporaryFileURL)) as! Self
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL { } else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import Photos import Photos
import MobileCoreServices import UniformTypeIdentifiers
import PencilKit import PencilKit
enum CompositionAttachmentData { enum CompositionAttachmentData {
@ -74,7 +74,7 @@ enum CompositionAttachmentData {
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])! data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
mimeType = "image/jpeg" mimeType = "image/jpeg"
} else { } else {
mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String mimeType = UTType(dataUTI)!.preferredMIMEType!
} }
completion(data, mimeType) completion(data, mimeType)

View File

@ -63,6 +63,7 @@ class Preferences: Codable, ObservableObject {
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts) self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType) self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions) self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType) self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
@ -97,6 +98,7 @@ class Preferences: Codable, ObservableObject {
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts) try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType) try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
try container.encode(grayscaleImages, forKey: .grayscaleImages) try container.encode(grayscaleImages, forKey: .grayscaleImages)
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
try container.encode(silentActions, forKey: .silentActions) try container.encode(silentActions, forKey: .silentActions)
try container.encode(statusContentType, forKey: .statusContentType) try container.encode(statusContentType, forKey: .statusContentType)
@ -133,6 +135,7 @@ class Preferences: Codable, ObservableObject {
@Published var showFavoriteAndReblogCounts = true @Published var showFavoriteAndReblogCounts = true
@Published var defaultNotificationsMode = NotificationsMode.allNotifications @Published var defaultNotificationsMode = NotificationsMode.allNotifications
@Published var grayscaleImages = false @Published var grayscaleImages = false
@Published var disableInfiniteScrolling = false
// MARK: Advanced // MARK: Advanced
@Published var silentActions: [String: Permission] = [:] @Published var silentActions: [String: Permission] = [:]
@ -165,6 +168,7 @@ class Preferences: Codable, ObservableObject {
case showFavoriteAndReblogCounts case showFavoriteAndReblogCounts
case defaultNotificationsType case defaultNotificationsType
case grayscaleImages case grayscaleImages
case disableInfiniteScrolling
case silentActions case silentActions
case statusContentType case statusContentType

View File

@ -12,7 +12,7 @@ import Photos
private let reuseIdentifier = "assetCell" private let reuseIdentifier = "assetCell"
private let cameraReuseIdentifier = "showCameraCell" private let cameraReuseIdentifier = "showCameraCell"
protocol AssetCollectionViewControllerDelegate: class { protocol AssetCollectionViewControllerDelegate: AnyObject {
func shouldSelectAsset(_ asset: PHAsset) -> Bool func shouldSelectAsset(_ asset: PHAsset) -> Bool
func didSelectAssets(_ assets: [PHAsset]) func didSelectAssets(_ assets: [PHAsset])
func captureFromCamera() func captureFromCamera()
@ -70,6 +70,7 @@ class AssetCollectionViewController: UICollectionViewController {
collectionView.alwaysBounceVertical = true collectionView.alwaysBounceVertical = true
collectionView.allowsMultipleSelection = true collectionView.allowsMultipleSelection = true
collectionView.allowsSelection = true
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier) collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier) collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
@ -98,8 +99,6 @@ class AssetCollectionViewController: UICollectionViewController {
} }
}) })
setEditing(true, animated: false)
updateItemsSelectedCount() updateItemsSelectedCount()
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: { if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Photos import Photos
protocol AssetPickerViewControllerDelegate: class { protocol AssetPickerViewControllerDelegate: AnyObject {
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData]) func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData])
} }

View File

@ -51,8 +51,14 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
} }
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
var isInteractivelyAnimatingDismissal: Bool = false {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
return true return !isInteractivelyAnimatingDismissal
} }
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .none return .none

View File

@ -41,6 +41,16 @@ struct ComposeAttachmentRow: View {
Label("Recognize Text", systemImage: "doc.text.viewfinder") Label("Recognize Text", systemImage: "doc.text.viewfinder")
} }
} }
if #available(iOS 15.0, *) {
Button(action: self.removeAttachment) {
Label("Delete", systemImage: "trash")
}.foregroundStyle(.red)
} else {
Button(action: self.removeAttachment) {
Label("Delete", systemImage: "trash")
}
}
} }
switch mode { switch mode {
@ -126,8 +136,10 @@ struct ComposeAttachmentRow: View {
} }
private func removeAttachment() { private func removeAttachment() {
withAnimation {
draft.attachments.removeAll { $0.id == attachment.id } draft.attachments.removeAll { $0.id == attachment.id }
} }
}
private func editDrawing() { private func editDrawing() {
uiState.composeDrawingMode = .edit(id: attachment.id) uiState.composeDrawingMode = .edit(id: attachment.id)

View File

@ -69,6 +69,7 @@ struct ComposeAttachmentsList: View {
.frame(height: cellHeight / 2) .frame(height: cellHeight / 2)
.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())
.frame(height: totalListHeight) .frame(height: totalListHeight)
.onAppear(perform: self.didAppear) .onAppear(perform: self.didAppear)
.onReceive(draft.$attachments, perform: self.attachmentsChanged) .onReceive(draft.$attachments, perform: self.attachmentsChanged)

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import PencilKit import PencilKit
protocol ComposeDrawingViewControllerDelegate: class { protocol ComposeDrawingViewControllerDelegate: AnyObject {
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)
} }
@ -23,6 +23,8 @@ class ComposeDrawingViewController: UIViewController {
private(set) var undoBarButtonItem: UIBarButtonItem! private(set) var undoBarButtonItem: UIBarButtonItem!
private(set) var redoBarButtonItem: UIBarButtonItem! private(set) var redoBarButtonItem: UIBarButtonItem!
private var toolPicker: PKToolPicker!
private var initialDrawing: PKDrawing? private var initialDrawing: PKDrawing?
init() { init() {
@ -70,27 +72,22 @@ class ComposeDrawingViewController: UIViewController {
canvasView.topAnchor.constraint(equalTo: view.topAnchor), canvasView.topAnchor.constraint(equalTo: view.topAnchor),
canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor) canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
]) ])
}
override func viewWillAppear(_ animated: Bool) { toolPicker = PKToolPicker()
super.viewWillAppear(animated)
// todo: should the PKToolPicker be owned by this VC or something else?
if let window = parent?.view.window, let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.setVisible(true, forFirstResponder: canvasView) toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView) toolPicker.addObserver(canvasView)
toolPicker.addObserver(self) toolPicker.addObserver(self)
updateLayout(for: toolPicker) updateLayout(for: toolPicker)
canvasView.becomeFirstResponder() canvasView.becomeFirstResponder()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// wait until the next run loop iteration so that the canvas view has become first responder and it's undo manager exists
DispatchQueue.main.async {
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidUndoChange, object: self.undoManager!) NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidUndoChange, object: self.undoManager!)
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidRedoChange, object: self.undoManager!) NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidRedoChange, object: self.undoManager!)
} }
}
}
func updateLayout(for toolPicker: PKToolPicker) { func updateLayout(for toolPicker: PKToolPicker) {
let obscuredFrame = toolPicker.frameObscured(in: view) let obscuredFrame = toolPicker.frameObscured(in: view)

View File

@ -11,7 +11,7 @@ import Combine
import Pachyderm import Pachyderm
import PencilKit import PencilKit
protocol ComposeHostingControllerDelegate: class { protocol ComposeHostingControllerDelegate: AnyObject {
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
} }
@ -245,6 +245,25 @@ extension ComposeHostingController: ComposeUIStateDelegate {
} }
func presentAssetPickerSheet() { func presentAssetPickerSheet() {
#if SDK_IOS_15
if #available(iOS 15.0, *) {
let picker = AssetPickerViewController()
picker.assetPickerDelegate = self
picker.modalPresentationStyle = .pageSheet
picker.overrideUserInterfaceStyle = .dark
let sheet = picker.sheetPresentationController!
sheet.detents = [.medium(), .large()]
sheet.prefersEdgeAttachedInCompactHeight = true
self.present(picker, animated: true)
} else {
presentOldAssetPickerSheet()
}
#else
presentOldAssetPickerSheet()
#endif
}
private func presentOldAssetPickerSheet() {
let sheetContainer = AssetPickerSheetContainerViewController() let sheetContainer = AssetPickerSheetContainerViewController()
sheetContainer.assetPicker.assetPickerDelegate = self sheetContainer.assetPicker.assetPickerDelegate = self
self.present(sheetContainer, animated: true) self.present(sheetContainer, animated: true)
@ -288,8 +307,10 @@ extension ComposeHostingController: AssetPickerViewControllerDelegate {
let attachments = attachments.map { let attachments = attachments.map {
CompositionAttachment(data: $0) CompositionAttachment(data: $0)
} }
withAnimation {
draft.attachments.append(contentsOf: attachments) draft.attachments.append(contentsOf: attachments)
} }
}
} }
extension ComposeHostingController: DraftsTableViewControllerDelegate { extension ComposeHostingController: DraftsTableViewControllerDelegate {

View File

@ -46,6 +46,7 @@ struct ComposePollView: View {
} }
.accentColor(buttonForegroundColor) .accentColor(buttonForegroundColor)
.background(Circle().foregroundColor(buttonBackgroundColor)) .background(Circle().foregroundColor(buttonBackgroundColor))
.hoverEffect()
} }
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
@ -161,6 +162,7 @@ struct ComposePollOption: View {
} }
.foregroundColor(poll.options.count == 1 ? .gray : .red) .foregroundColor(poll.options.count == 1 ? .gray : .red)
.disabled(poll.options.count == 1) .disabled(poll.options.count == 1)
.hoverEffect()
} }
} }

View File

@ -52,9 +52,20 @@ struct ComposeReplyView: View {
private func replyAvatarImage(geometry: GeometryProxy) -> some View { private func replyAvatarImage(geometry: GeometryProxy) -> some View {
// using named coordinate spaces produces an incorrect scroll offset on iOS 13, // using named coordinate spaces produces an incorrect scroll offset on iOS 13,
// so simply compare the geometry inside and outside the scroll view in the global coordinate space // so simply compare the geometry inside and outside the scroll view in the global coordinate space
var scrollOffset = outerMinY - geometry.frame(in: .global).minY let scrollOffset = outerMinY - geometry.frame(in: .global).minY
scrollOffset += stackPadding
let offset = min(max(scrollOffset, 0), geometry.size.height - 50 - stackPadding) // add stackPadding so that the image is always at least stackPadding away from the top
var offset = scrollOffset + stackPadding
// offset can never be less than 0 (i.e., above the top of the in-reply-to content)
offset = max(offset, 0)
// subtract 50, because we care about where the bottom of the view is but the offset is relative to the top of the view
let maxOffset = (contentHeight ?? 0) - 50
// once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content
offset = min(offset, maxOffset)
return ComposeAvatarImageView(url: status.account.avatar) return ComposeAvatarImageView(url: status.account.avatar)
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50) .cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
protocol ComposeTextViewCaretScrolling: class { protocol ComposeTextViewCaretScrolling: AnyObject {
var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set } var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set }
} }

View File

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
protocol ComposeUIStateDelegate: class { protocol ComposeUIStateDelegate: AnyObject {
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get } var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
func dismissCompose(mode: ComposeUIState.DismissMode) func dismissCompose(mode: ComposeUIState.DismissMode)
@ -60,6 +60,6 @@ extension ComposeUIState {
} }
} }
protocol ComposeAutocompleteHandler: class { protocol ComposeAutocompleteHandler: AnyObject {
func autocomplete(with string: String) func autocomplete(with string: String)
} }

View File

@ -58,8 +58,10 @@ struct ComposeView: View {
} }
} }
if postProgress > 0 {
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149 // can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
WrappedProgressView(value: postProgress, total: postTotalProgress) WrappedProgressView(value: postProgress, total: postTotalProgress)
}
autocompleteSuggestions autocompleteSuggestions
} }

View File

@ -11,7 +11,7 @@ import Pachyderm
private let reuseIdentifier = "EmojiCell" private let reuseIdentifier = "EmojiCell"
protocol EmojiPickerCollectionViewControllerDelegate: class { protocol EmojiPickerCollectionViewControllerDelegate: AnyObject {
func selectedEmoji(_ emoji: Emoji) func selectedEmoji(_ emoji: Emoji)
} }

View File

@ -132,6 +132,30 @@ class ConversationTableViewController: EnhancedTableViewController {
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed)) visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
navigationItem.rightBarButtonItem = visibilityBarButtonItem navigationItem.rightBarButtonItem = visibilityBarButtonItem
loadMainStatus()
}
private func loadMainStatus() {
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
self.mainStatusLoaded(mainStatus)
} else {
let request = Client.getStatus(id: mainStatusID)
mastodonController.run(request) { (response) in
switch response {
case let .success(status, _):
let viewContext = self.mastodonController.persistentContainer.viewContext
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false, context: viewContext) { (statusMO) in
self.mainStatusLoaded(statusMO)
}
case .failure(_):
fatalError()
}
}
}
}
private func mainStatusLoaded(_ mainStatus: StatusMO) {
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState) let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
@ -139,12 +163,10 @@ class ConversationTableViewController: EnhancedTableViewController {
snapshot.appendItems([mainStatusItem], toSection: .statuses) snapshot.appendItems([mainStatusItem], toSection: .statuses)
dataSource.apply(snapshot, animatingDifferences: false) dataSource.apply(snapshot, animatingDifferences: false)
guard let mainStatus = self.mastodonController.persistentContainer.status(for: self.mainStatusID) else {
fatalError("Missing cached status \(self.mainStatusID)")
}
let mainStatusInReplyToID = mainStatus.inReplyToID let mainStatusInReplyToID = mainStatus.inReplyToID
mainStatus.incrementReferenceCount() mainStatus.incrementReferenceCount()
// todo: it would be nice to cache these contexts
let request = Status.getContext(mainStatusID) let request = Status.getContext(mainStatusID)
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(context, _) = response else { fatalError() } guard case let .success(context, _) = response else { fatalError() }
@ -304,7 +326,16 @@ class ConversationTableViewController: EnhancedTableViewController {
} }
@objc func toggleVisibilityButtonPressed() { @objc func toggleVisibilityButtonPressed() {
#if SDK_IOS_15
if #available(iOS 15.0, *) {
visibilityBarButtonItem.isSelected = !visibilityBarButtonItem.isSelected
showStatusesAutomatically = visibilityBarButtonItem.isSelected
} else {
showStatusesAutomatically = !showStatusesAutomatically showStatusesAutomatically = !showStatusesAutomatically
}
#else
showStatusesAutomatically = !showStatusesAutomatically
#endif
let snapshot = dataSource.snapshot() let snapshot = dataSource.snapshot()
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true { for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
protocol DraftsTableViewControllerDelegate: class { protocol DraftsTableViewControllerDelegate: AnyObject {
func draftSelectionCanceled() func draftSelectionCanceled()
func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void) func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void)
func draftSelected(_ draft: Draft) func draftSelected(_ draft: Draft)

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
protocol FastAccountSwitcherViewControllerDelegate: class { protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
} }

View File

@ -51,8 +51,14 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
private var prevZoomScale: CGFloat? private var prevZoomScale: CGFloat?
private var isGrayscale = false private var isGrayscale = false
var isInteractivelyAnimatingDismissal: Bool = false {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
return true return !isInteractivelyAnimatingDismissal
} }
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .none return .none

View File

@ -43,8 +43,14 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
var animationGifData: Data? { largeImageVC?.animationGifData } var animationGifData: Data? { largeImageVC?.animationGifData }
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
var isInteractivelyAnimatingDismissal: Bool = false {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
return true return !isInteractivelyAnimatingDismissal
} }
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .none return .none

View File

@ -15,6 +15,7 @@ protocol LargeImageAnimatableViewController: UIViewController {
var animationImage: UIImage? { get } var animationImage: UIImage? { get }
var animationGifData: Data? { get } var animationGifData: Data? { get }
var dismissInteractionController: LargeImageInteractionController? { get } var dismissInteractionController: LargeImageInteractionController? { get }
var isInteractivelyAnimatingDismissal: Bool { get set }
} }
extension LargeImageAnimatableViewController { extension LargeImageAnimatableViewController {
@ -74,7 +75,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
toVC.largeImageController?.contentView.isHidden = true toVC.largeImageController?.contentView.isHidden = true
toVC.largeImageController?.setControlsVisible(false, animated: false) toVC.largeImageController?.setControlsVisible(false, animated: false)
var finalFrameSize = finalVCFrame.inset(by: fromVC.view.safeAreaInsets).size var finalFrameSize = finalVCFrame.inset(by: toVC.view.safeAreaInsets).size
let newWidth = finalFrameSize.width / image.size.width let newWidth = finalFrameSize.width / image.size.width
let newHeight = finalFrameSize.height / image.size.height let newHeight = finalFrameSize.height / image.size.height
if newHeight < newWidth { if newHeight < newWidth {

View File

@ -14,9 +14,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
var direction: CGFloat? var direction: CGFloat?
var shouldCompleteTransition = false var shouldCompleteTransition = false
private weak var viewController: UIViewController! private weak var viewController: LargeImageAnimatableViewController!
init(viewController: UIViewController) { init(viewController: LargeImageAnimatableViewController) {
super.init() super.init()
self.viewController = viewController self.viewController = viewController
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:))) let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
@ -42,6 +42,7 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
viewController.dismiss(animated: true) viewController.dismiss(animated: true)
case .changed: case .changed:
shouldCompleteTransition = progress > 0.5 || velocity > 1000 shouldCompleteTransition = progress > 0.5 || velocity > 1000
viewController.isInteractivelyAnimatingDismissal = progress > 0.1
update(progress) update(progress)
case .cancelled: case .cancelled:
inProgress = false inProgress = false
@ -59,4 +60,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
} }
} }
override func cancel() {
super.cancel()
viewController.isInteractivelyAnimatingDismissal = false
}
} }

View File

@ -58,7 +58,7 @@ class ListTimelineViewController: TimelineTableViewController {
dismiss(animated: true) dismiss(animated: true)
// todo: show loading indicator // todo: show loading indicator
reloadInitialItems() reloadInitial()
} }
} }

View File

@ -71,14 +71,21 @@ extension AccountSwitchingContainerViewController {
extension AccountSwitchingContainerViewController: TuskerRootViewController { extension AccountSwitchingContainerViewController: TuskerRootViewController {
func presentCompose() { func presentCompose() {
loadViewIfNeeded()
root.presentCompose() root.presentCompose()
} }
func select(tab: MainTabBarViewController.Tab) { func select(tab: MainTabBarViewController.Tab) {
loadViewIfNeeded()
root.select(tab: tab) root.select(tab: tab)
} }
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? { func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
root.getTabController(tab: tab) loadViewIfNeeded()
return root.getTabController(tab: tab)
}
func performSearch(query: String) {
root.performSearch(query: query)
} }
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
protocol MainSidebarViewControllerDelegate: class { protocol MainSidebarViewControllerDelegate: AnyObject {
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
} }
@ -48,7 +48,7 @@ class MainSidebarViewController: UIViewController {
private(set) var previouslySelectedItem: Item? private(set) var previouslySelectedItem: Item?
var selectedItem: Item? { var selectedItem: Item? {
guard let indexPath = collectionView.indexPathsForSelectedItems?.first else { guard let indexPath = collectionView?.indexPathsForSelectedItems?.first else {
return nil return nil
} }
return dataSource.itemIdentifier(for: indexPath) return dataSource.itemIdentifier(for: indexPath)
@ -448,6 +448,20 @@ extension MainSidebarViewController: UICollectionViewDelegate {
sidebarDelegate?.sidebar(self, didSelectItem: item) sidebarDelegate?.sidebar(self, didSelectItem: item)
} }
} }
@available(iOS 15.0, *)
func collectionView(_ collectionView: UICollectionView, selectionFollowsFocusForItemAt indexPath: IndexPath) -> Bool {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return true
}
// don't immediately select items that present VCs when the they're focused, only when deliberately selected
switch item {
case .tab(.compose), .addList, .addSavedHashtag, .addSavedInstance:
return false
default:
return true
}
}
} }
extension MainSidebarViewController: UICollectionViewDragDelegate { extension MainSidebarViewController: UICollectionViewDragDelegate {

View File

@ -19,6 +19,10 @@ class MainSplitViewController: UISplitViewController {
private var tabBarViewController: MainTabBarViewController! private var tabBarViewController: MainTabBarViewController!
private var secondaryNavController: UINavigationController! {
viewController(for: .secondary) as? UINavigationController
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {
return .portrait return .portrait
@ -67,8 +71,7 @@ class MainSplitViewController: UISplitViewController {
} }
func select(item: MainSidebarViewController.Item) { func select(item: MainSidebarViewController.Item) {
let nav = viewController(for: .secondary) as! UINavigationController secondaryNavController.viewControllers = getOrCreateNavigationStack(item: item)
nav.viewControllers = getOrCreateNavigationStack(item: item)
} }
func getOrCreateNavigationStack(item: MainSidebarViewController.Item) -> [UIViewController] { func getOrCreateNavigationStack(item: MainSidebarViewController.Item) -> [UIViewController] {
@ -109,8 +112,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
private func transferNavigationStack(from item: MainSidebarViewController.Item, to destination: UINavigationController, dropFirst: Bool = false, append: Bool = false) { private func transferNavigationStack(from item: MainSidebarViewController.Item, to destination: UINavigationController, dropFirst: Bool = false, append: Bool = false) {
var itemNavStack: [UIViewController] var itemNavStack: [UIViewController]
if item == sidebar.selectedItem { if item == sidebar.selectedItem {
let detailNav = viewController(for: .secondary) as! UINavigationController itemNavStack = secondaryNavController.viewControllers
itemNavStack = detailNav.viewControllers
} else { } else {
itemNavStack = navigationStacks[item] ?? [] itemNavStack = navigationStacks[item] ?? []
navigationStacks.removeValue(forKey: item) navigationStacks.removeValue(forKey: item)
@ -190,8 +192,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// Make sure viewDidLoad is called so that the searchController/resultsController have been initialized // Make sure viewDidLoad is called so that the searchController/resultsController have been initialized
explore.loadViewIfNeeded() explore.loadViewIfNeeded()
let nav = viewController(for: .secondary) as! UINavigationController let search = secondaryNavController.viewControllers.first as! SearchViewController
let search = nav.viewControllers.first as! SearchViewController
// Copy the search query from the search VC to the Explore VC's search controller. // Copy the search query from the search VC to the Explore VC's search controller.
let query = search.searchController.searchBar.text ?? "" let query = search.searchController.searchBar.text ?? ""
explore.searchController.searchBar.text = query explore.searchController.searchBar.text = query
@ -318,9 +319,8 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
} }
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) { func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) {
let nav = viewController(for: .secondary) as! UINavigationController
if let previous = sidebar.previouslySelectedItem { if let previous = sidebar.previouslySelectedItem {
navigationStacks[previous] = nav.viewControllers navigationStacks[previous] = secondaryNavController.viewControllers
} }
select(item: item) select(item: item)
} }
@ -353,11 +353,18 @@ fileprivate extension MainSidebarViewController.Item {
extension MainSplitViewController: TuskerRootViewController { extension MainSplitViewController: TuskerRootViewController {
@objc func presentCompose() { @objc func presentCompose() {
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
} else {
let vc = ComposeHostingController(mastodonController: mastodonController) let vc = ComposeHostingController(mastodonController: mastodonController)
let nav = EnhancedNavigationViewController(rootViewController: vc) let nav = EnhancedNavigationViewController(rootViewController: vc)
nav.presentationController?.delegate = vc nav.presentationController?.delegate = vc
present(nav, animated: true) present(nav, animated: true)
} }
}
func select(tab: MainTabBarViewController.Tab) { func select(tab: MainTabBarViewController.Tab) {
if traitCollection.horizontalSizeClass == .compact { if traitCollection.horizontalSizeClass == .compact {
@ -365,12 +372,19 @@ extension MainSplitViewController: TuskerRootViewController {
} else { } else {
if tab == .compose { if tab == .compose {
presentCompose() presentCompose()
} else {
if presentedViewController != nil {
dismiss(animated: true) {
self.select(item: .tab(tab))
self.sidebar.select(item: .tab(tab), animated: false)
}
} else { } else {
select(item: .tab(tab)) select(item: .tab(tab))
sidebar.select(item: .tab(tab), animated: false) sidebar.select(item: .tab(tab), animated: false)
} }
} }
} }
}
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? { func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
if traitCollection.horizontalSizeClass == .compact { if traitCollection.horizontalSizeClass == .compact {
@ -379,12 +393,43 @@ extension MainSplitViewController: TuskerRootViewController {
if tab == .compose { if tab == .compose {
return nil return nil
} else if case .tab(tab) = sidebar.selectedItem { } else if case .tab(tab) = sidebar.selectedItem {
return viewController(for: .secondary) return secondaryNavController
} else { } else {
return nil return nil
} }
} }
} }
func performSearch(query: String) {
guard traitCollection.horizontalSizeClass != .compact else {
// ensure the tab bar VC is loaded
loadViewIfNeeded()
tabBarViewController.performSearch(query: query)
return
}
if sidebar.selectedItem != .search {
select(item: .search)
}
guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else {
return
}
secondaryNavController.popToRootViewController(animated: false)
if searchViewController.isViewLoaded {
DispatchQueue.main.async {
searchViewController.searchController.isActive = true
}
} else {
searchViewController.searchControllerStatusOnAppearance = true
searchViewController.loadViewIfNeeded()
}
searchViewController.searchController.searchBar.text = query
searchViewController.resultsController.performSearch(query: query)
}
} }
extension MainSplitViewController: BackgroundableViewController { extension MainSplitViewController: BackgroundableViewController {

View File

@ -7,7 +7,6 @@
// //
import UIKit import UIKit
import SwiftUI
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
@ -16,6 +15,9 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
private var composePlaceholder: UIViewController! private var composePlaceholder: UIViewController!
private var fastAccountSwitcher: FastAccountSwitcherViewController! private var fastAccountSwitcher: FastAccountSwitcherViewController!
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
var selectedTab: Tab { var selectedTab: Tab {
return Tab(rawValue: selectedIndex)! return Tab(rawValue: selectedIndex)!
} }
@ -71,9 +73,62 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
tapRecognizer.cancelsTouchesInView = false tapRecognizer.cancelsTouchesInView = false
tabBar.addGestureRecognizer(tapRecognizer) tabBar.addGestureRecognizer(tapRecognizer)
if findMyProfileTabBarButton() != nil {
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(fastSwitcherIndicator)
NSLayoutConstraint.activate([
fastSwitcherIndicator.widthAnchor.constraint(equalToConstant: 10),
fastSwitcherIndicator.heightAnchor.constraint(equalToConstant: 12),
])
}
tabBar.isSpringLoaded = true tabBar.isSpringLoaded = true
} }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
repositionFastSwitcherIndicator()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
repositionFastSwitcherIndicator()
}
private func repositionFastSwitcherIndicator() {
guard let myProfileButton = findMyProfileTabBarButton() else {
return
}
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
// using interfaceOrientation isn't ideal, but UITabBar buttons may lay out horizontally even in the compact size class
if traitCollection.horizontalSizeClass == .compact && interfaceOrientation.isPortrait {
fastSwitcherConstraints = [
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4),
// tab bar button image width is 30
fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2),
]
} else {
fastSwitcherConstraints = [
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor),
fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor),
]
}
NSLayoutConstraint.activate(fastSwitcherConstraints)
}
private func findMyProfileTabBarButton() -> UIView? {
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
// sanity check that there is 1 button per VC
guard tabBarButtons.count == viewControllers!.count,
let myProfileButton = tabBarButtons.last else {
return nil
}
return myProfileButton
}
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) { @objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
fastAccountSwitcher.hide() fastAccountSwitcher.hide()
} }
@ -138,6 +193,8 @@ extension MainTabBarViewController {
if tab == .compose { if tab == .compose {
return nil return nil
} else { } else {
// viewWControllers array is setup in viewDidLoad
loadViewIfNeeded()
return viewControllers![tab.rawValue] return viewControllers![tab.rawValue]
} }
} }
@ -145,13 +202,9 @@ extension MainTabBarViewController {
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate { extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") } guard let myProfileButton = findMyProfileTabBarButton() else {
// sanity check that there is 1 button per VC
guard tabBarButtons.count == viewControllers!.count,
let myProfileButton = tabBarButtons.last else {
return false return false
} }
let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view) let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view)
return myProfileButton.bounds.contains(locationInButton) return myProfileButton.bounds.contains(locationInButton)
} }
@ -159,19 +212,56 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
extension MainTabBarViewController: TuskerRootViewController { extension MainTabBarViewController: TuskerRootViewController {
@objc func presentCompose() { @objc func presentCompose() {
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
} else {
let vc = ComposeHostingController(mastodonController: mastodonController) let vc = ComposeHostingController(mastodonController: mastodonController)
let nav = EnhancedNavigationViewController(rootViewController: vc) let nav = EnhancedNavigationViewController(rootViewController: vc)
nav.presentationController?.delegate = vc nav.presentationController?.delegate = vc
present(nav, animated: true) present(nav, animated: true)
} }
}
func select(tab: Tab) { func select(tab: Tab) {
if tab == .compose { if tab == .compose {
presentCompose() presentCompose()
} else {
// when switching tabs, dismiss the currently presented VC
// otherwise the selected tab changes behind the presented VC
if presentedViewController != nil {
dismiss(animated: true) {
self.selectedIndex = tab.rawValue
}
} else { } else {
selectedIndex = tab.rawValue selectedIndex = tab.rawValue
} }
} }
}
func performSearch(query: String) {
guard let exploreNavController = getTabController(tab: .explore) as? UINavigationController,
let exploreController = exploreNavController.viewControllers.first as? ExploreViewController else {
return
}
select(tab: .explore)
exploreNavController.popToRootViewController(animated: false)
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time
if exploreController.isViewLoaded {
exploreController.searchController.isActive = true
} else {
exploreController.searchControllerStatusOnAppearance = true
// we still need to load the view so that we can setup the search query
exploreController.loadViewIfNeeded()
}
exploreController.searchController.searchBar.text = query
exploreController.resultsController.performSearch(query: query)
}
} }
extension MainTabBarViewController: BackgroundableViewController { extension MainTabBarViewController: BackgroundableViewController {

View File

@ -12,4 +12,5 @@ protocol TuskerRootViewController: UIViewController {
func presentCompose() func presentCompose()
func select(tab: MainTabBarViewController.Tab) func select(tab: MainTabBarViewController.Tab)
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
func performSearch(query: String)
} }

View File

@ -10,7 +10,7 @@ import UIKit
import Combine import Combine
import Pachyderm import Pachyderm
protocol InstanceSelectorTableViewControllerDelegate: class { protocol InstanceSelectorTableViewControllerDelegate: AnyObject {
func didSelectInstance(url: URL) func didSelectInstance(url: URL)
} }

View File

@ -36,6 +36,9 @@ struct PreferencesView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
}.onDrag {
let activity = UserActivityManager.mainSceneActivity(accountID: account.id)
return NSItemProvider(object: activity)
} }
}.onDelete { (indices: IndexSet) in }.onDelete { (indices: IndexSet) in
var indices = indices var indices = indices

View File

@ -16,6 +16,7 @@ struct WellnessPrefsView: View {
showFavAndReblogCount showFavAndReblogCount
notificationsMode notificationsMode
grayscaleImages grayscaleImages
disableInfiniteScrolling
} }
.listStyle(InsetGroupedListStyle()) .listStyle(InsetGroupedListStyle())
.navigationBarTitle(Text("Digital Wellness")) .navigationBarTitle(Text("Digital Wellness"))
@ -46,6 +47,14 @@ struct WellnessPrefsView: View {
} }
} }
} }
private var disableInfiniteScrolling: some View {
Section(footer: Text("Require a button tap before loading more posts.")) {
Toggle(isOn: $preferences.disableInfiniteScrolling) {
Text("Disable Infinite Scrolling")
}
}
}
} }
struct WellnessPrefsView_Previews: PreviewProvider { struct WellnessPrefsView_Previews: PreviewProvider {

View File

@ -91,11 +91,18 @@ class ProfileViewController: UIPageViewController {
addKeyCommand(MenuController.nextSubTabCommand) addKeyCommand(MenuController.nextSubTabCommand)
accountUpdater = mastodonController.persistentContainer.accountSubject accountUpdater = mastodonController.persistentContainer.accountSubject
.filter { [weak self] in $0 == self?.accountID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.filter { [weak self] in $0 == self?.accountID }
.sink { [weak self] (_) in self?.updateAccountUI() } .sink { [weak self] (_) in self?.updateAccountUI() }
loadAccount() loadAccount()
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
if let nav = navigationController {
let appearance = UINavigationBarAppearance()
appearance.configureWithDefaultBackground()
nav.navigationBar.scrollEdgeAppearance = appearance
}
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
@ -245,34 +252,6 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
view.pagesSegmentedControl.isUserInteractionEnabled = true view.pagesSegmentedControl.isUserInteractionEnabled = true
} }
} }
func profileHeader(_ view: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView) {
let account = mastodonController.persistentContainer.account(for: accountID)!
func showActivityController(activities: [UIActivity]) {
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities)
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: account.url)
activityController.popoverPresentationController?.sourceView = sourceView
self.present(activityController, animated: true)
}
if account.id == mastodonController.account.id {
showActivityController(activities: [OpenInSafariActivity()])
} else {
let request = Client.getRelationships(accounts: [account.id])
mastodonController.run(request) { (response) in
var customActivities: [UIActivity] = [OpenInSafariActivity()]
if case let .success(results, _) = response, let relationship = results.first {
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
customActivities.insert(toggleFollowActivity, at: 0)
}
DispatchQueue.main.async {
showActivityController(activities: customActivities)
}
}
}
}
} }
extension ProfileViewController: TabBarScrollableViewController { extension ProfileViewController: TabBarScrollableViewController {

View File

@ -14,7 +14,7 @@ fileprivate let accountCell = "accountCell"
fileprivate let statusCell = "statusCell" fileprivate let statusCell = "statusCell"
fileprivate let hashtagCell = "hashtagCell" fileprivate let hashtagCell = "hashtagCell"
protocol SearchResultsViewControllerDelegate: class { protocol SearchResultsViewControllerDelegate: AnyObject {
func selectedSearchResult(account accountID: String) func selectedSearchResult(account accountID: String)
func selectedSearchResult(hashtag: Hashtag) func selectedSearchResult(hashtag: Hashtag)
func selectedSearchResult(status statusID: String) func selectedSearchResult(status statusID: String)
@ -36,6 +36,7 @@ class SearchResultsViewController: EnhancedTableViewController {
var dataSource: UITableViewDiffableDataSource<Section, Item>! var dataSource: UITableViewDiffableDataSource<Section, Item>!
private var activityIndicator: UIActivityIndicatorView! private var activityIndicator: UIActivityIndicatorView!
private var errorLabel: UILabel!
/// Types of results to search for. `nil` means all results will be included. /// Types of results to search for. `nil` means all results will be included.
var resultTypes: [SearchResultType]? = nil var resultTypes: [SearchResultType]? = nil
@ -61,6 +62,22 @@ class SearchResultsViewController: EnhancedTableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
errorLabel = UILabel()
errorLabel.translatesAutoresizingMaskIntoConstraints = false
errorLabel.font = .preferredFont(forTextStyle: .callout)
errorLabel.textColor = .secondaryLabel
errorLabel.numberOfLines = 0
errorLabel.textAlignment = .center
errorLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
tableView.addSubview(errorLabel)
NSLayoutConstraint.activate([
errorLabel.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
errorLabel.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
errorLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: tableView.leadingAnchor, multiplier: 1),
tableView.trailingAnchor.constraint(equalToSystemSpacingAfter: errorLabel.trailingAnchor, multiplier: 1),
])
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell) tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell) tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell) tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell)
@ -131,19 +148,30 @@ class SearchResultsViewController: EnhancedTableViewController {
activityIndicator.isHidden = false activityIndicator.isHidden = false
activityIndicator.startAnimating() activityIndicator.startAnimating()
errorLabel.isHidden = true
let resultTypes = self.resultTypes
let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10) let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(results, _) = response else { fatalError() } switch response {
case let .success(results, _):
guard self.currentQuery == query else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
self.activityIndicator.isHidden = true self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating() self.activityIndicator.stopAnimating()
} }
self.showSearchResults(results)
case let .failure(error):
DispatchQueue.main.async {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
guard self.currentQuery == query else { return } self.showSearchError(error)
}
}
}
}
private func showSearchResults(_ results: SearchResults) {
let oldSnapshot = self.dataSource.snapshot() let oldSnapshot = self.dataSource.snapshot()
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
@ -162,6 +190,7 @@ class SearchResultsViewController: EnhancedTableViewController {
} }
} }
let resultTypes = self.resultTypes
if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) { if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) {
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts) snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
@ -178,10 +207,18 @@ class SearchResultsViewController: EnhancedTableViewController {
} }
}, completion: { }, completion: {
DispatchQueue.main.async { DispatchQueue.main.async {
self.errorLabel.isHidden = true
self.dataSource.apply(snapshot) self.dataSource.apply(snapshot)
} }
}) })
} }
private func showSearchError(_ error: Client.Error) {
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
dataSource.apply(snapshot)
errorLabel.isHidden = false
errorLabel.text = error.localizedDescription
} }
// MARK: - Table view delegate // MARK: - Table view delegate

View File

@ -37,9 +37,10 @@ class SearchViewController: UIViewController {
resultsController = SearchResultsViewController(mastodonController: mastodonController) resultsController = SearchResultsViewController(mastodonController: mastodonController)
resultsController.exploreNavigationController = self.navigationController resultsController.exploreNavigationController = self.navigationController
searchController = UISearchController(searchResultsController: resultsController) searchController = UISearchController(searchResultsController: resultsController)
searchController.searchResultsUpdater = resultsController searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.autocapitalizationType = .none searchController.searchBar.autocapitalizationType = .none
searchController.searchBar.delegate = resultsController searchController.searchBar.delegate = resultsController
searchController.hidesNavigationBarDuringPresentation = false
definesPresentationContext = true definesPresentationContext = true
navigationItem.searchController = searchController navigationItem.searchController = searchController

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
protocol InstanceTimelineViewControllerDelegate: class { protocol InstanceTimelineViewControllerDelegate: AnyObject {
func didSaveInstance(url: URL) func didSaveInstance(url: URL)
func didUnsaveInstance(url: URL) func didUnsaveInstance(url: URL)
} }

View File

@ -11,7 +11,7 @@ import Pachyderm
typealias TimelineEntry = (id: String, state: StatusState) typealias TimelineEntry = (id: String, state: StatusState)
class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry> { class TimelineTableViewController: DiffableTimelineLikeTableViewController<TimelineTableViewController.Section, TimelineTableViewController.Item> {
let timeline: Timeline let timeline: Timeline
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
@ -19,6 +19,8 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
private var newer: RequestRange? private var newer: RequestRange?
private var older: RequestRange? private var older: RequestRange?
private var didConfirmLoadMore = false
init(for timeline: Timeline, mastodonController: MastodonController) { init(for timeline: Timeline, mastodonController: MastodonController) {
self.timeline = timeline self.timeline = timeline
self.mastodonController = mastodonController self.mastodonController = mastodonController
@ -40,115 +42,172 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
} }
deinit { deinit {
guard let persistentContainer = mastodonController?.persistentContainer else { return } guard let persistentContainer = mastodonController?.persistentContainer,
let dataSource = dataSource else { return }
// decrement reference counts of any statuses we still have // decrement reference counts of any statuses we still have
// if the app is currently being quit, this will not affect the persisted data because // if the app is currently being quit, this will not affect the persisted data because
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:) // the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
for section in sections { // todo: remove the whole reference count system
for (id, _) in section { for case let .status(id: id, state: _) in dataSource.snapshot().itemIdentifiers {
persistentContainer.status(for: id)?.decrementReferenceCount() persistentContainer.status(for: id)?.decrementReferenceCount()
} }
} }
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell") tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
tableView.register(UINib(nibName: "ConfirmLoadMoreTableViewCell", bundle: .main), forCellReuseIdentifier: "confirmLoadMoreCell")
} }
// MARK: - DiffableTimelineLikeTableViewController
override class func refreshCommandTitle() -> String { override class func refreshCommandTitle() -> String {
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title") return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
} }
override func willRemoveRows(at indexPaths: [IndexPath]) { override func timelineContentSections() -> [Section] {
for indexPath in indexPaths { return [.statuses]
let id = item(for: indexPath).id }
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
switch item {
case let .status(id: id, state: state):
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell
case .confirmLoadMore:
let cell = tableView.dequeueReusableCell(withIdentifier: "confirmLoadMoreCell", for: indexPath) as! ConfirmLoadMoreTableViewCell
cell.confirmLoadMore = {
self.didConfirmLoadMore = true
self.loadOlder()
self.didConfirmLoadMore = false
}
return cell
} }
} }
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) { override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
let request = Client.getStatuses(timeline: timeline) guard let mastodonController = mastodonController else {
completion(.failure(.noClient))
mastodonController?.run(request) { (response) in
guard case let .success(statuses, pagination) = response else {
completion([])
return return
} }
let request = Client.getStatuses(timeline: timeline)
mastodonController.run(request) { response in
switch response {
case let .failure(error):
completion(.failure(.client(error)))
case let .success(statuses, pagination):
self.newer = pagination?.newer self.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
self.mastodonController?.persistentContainer.addAll(statuses: statuses) { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
completion(statuses.map { ($0.id, .unknown) }) var snapshot = Snapshot()
snapshot.appendSections([.statuses, .footer])
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
completion(.success(snapshot))
}
} }
} }
} }
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) { override func loadOlderItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
guard let older = older else { guard let older = older else {
completion([]) completion(.failure(.noOlder))
return
}
guard !Preferences.shared.disableInfiniteScrolling || didConfirmLoadMore else {
guard !currentSnapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
// todo: need something more accurate than "success"/"failure"
completion(.success(currentSnapshot))
return
}
var snapshot = currentSnapshot
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
self.dataSource.apply(snapshot)
completion(.success(snapshot))
return return
} }
let request = Client.getStatuses(timeline: timeline, range: older) let request = Client.getStatuses(timeline: timeline, range: older)
mastodonController.run(request) { (response) in mastodonController.run(request) { response in
guard case let .success(statuses, pagination) = response else { switch response {
completion([]) case let .failure(error):
return completion(.failure(.client(error)))
}
case let .success(statuses, pagination):
self.older = pagination?.older self.older = pagination?.older
self.mastodonController?.persistentContainer.addAll(statuses: statuses) { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
completion(statuses.map { ($0.id, .unknown) }) var snapshot = currentSnapshot
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
snapshot.deleteItems([.confirmLoadMore])
completion(.success(snapshot))
}
} }
} }
} }
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) { override func loadNewerItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
guard let newer = newer else { guard let newer = newer else {
completion([]) completion(.failure(.noNewer))
return return
} }
let request = Client.getStatuses(timeline: timeline, range: newer) let request = Client.getStatuses(timeline: timeline, range: newer)
mastodonController.run(request) { response in
switch response {
case let .failure(error):
completion(.failure(.client(error)))
mastodonController.run(request) { (response) in case let .success(statuses, pagination):
guard case let .success(statuses, pagination) = response else {
completion([])
return
}
// if there are no new statuses, pagination is nil // if there are no new statuses, pagination is nil
// if we were to then overwrite self.newer, future refreshes would fail // if we were to then overwrite self.newer, future refresh would fail
if let newer = pagination?.newer { if let newer = pagination?.newer {
self.newer = newer self.newer = newer
} }
self.mastodonController?.persistentContainer.addAll(statuses: statuses) { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
completion(statuses.map { ($0.id, .unknown) }) var snapshot = currentSnapshot
let identifiers = statuses.map { Item.status(id: $0.id, state: .unknown) }
if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
snapshot.insertItems(identifiers, beforeItem: first)
} else {
snapshot.appendItems(identifiers, toSection: .statuses)
}
completion(.success(snapshot))
}
} }
} }
} }
// MARK: - UITableViewDataSource override func willRemoveItems(_ items: [Item]) {
for case let .status(id: id, state: _) in items {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } }
let (id, state) = item(for: indexPath)
cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell
} }
} }
extension TimelineTableViewController {
enum Section: Hashable, CaseIterable {
case statuses
case footer
}
enum Item: Hashable {
case status(id: String, state: StatusState)
case confirmLoadMore
}
}
extension TimelineTableViewController: TuskerNavigationDelegate { extension TimelineTableViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController } var apiController: MastodonController { mastodonController }
} }
@ -161,17 +220,23 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let ids = indexPaths.map { item(for: $0).id } let ids: [String] = indexPaths.compactMap {
if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
return id
} else {
return nil
}
}
prefetchStatuses(with: ids) prefetchStatuses(with: ids)
} }
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
let ids: [String] = indexPaths.compactMap { let ids: [String] = indexPaths.compactMap {
guard $0.section < sections.count, if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
$0.row < sections[$0.section].count else { return id
} else {
return nil return nil
} }
return item(for: $0).id
} }
cancelPrefetchingStatuses(with: ids) cancelPrefetchingStatuses(with: ids)
} }

View File

@ -0,0 +1,269 @@
//
// DiffableTimelineLikeTableViewController.swift
// Tusker
//
// Created by Shadowfacts on 6/18/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable, Item: Hashable>: EnhancedTableViewController, RefreshableViewController {
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
typealias LoadResult = Result<Snapshot, LoadError>
private let pageSize = 20
private(set) var state = State.unloaded
private var lastLastVisibleRow: IndexPath?
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
init() {
super.init(style: .plain)
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: Self.refreshCommandTitle()))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: self.cellProvider)
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140
#if !targetEnvironment(macCatalyst)
self.refreshControl = UIRefreshControl()
self.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
#endif
if let prefetchSource = self as? UITableViewDataSourcePrefetching {
tableView.prefetchDataSource = prefetchSource
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadInitial()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
pruneOffscreenRows()
}
class func refreshCommandTitle() -> String {
return "Refresh"
}
private func pruneOffscreenRows() {
guard let lastVisibleRow = lastLastVisibleRow else {
return
}
var snapshot = dataSource.snapshot()
let lastVisibleRowSection = snapshot.sectionIdentifiers[lastVisibleRow.section]
let contentSections = snapshot.sectionIdentifiers.filter { timelineContentSections().contains($0) }
guard let lastVisibleContentSectionIndex = contentSections.lastIndex(of: lastVisibleRowSection) else {
return
}
if lastVisibleContentSectionIndex < contentSections.count - 1 {
// there are more content sections below the current last visible one
let sectionsToRemove = contentSections[lastVisibleContentSectionIndex...]
snapshot.deleteSections(Array(sectionsToRemove))
willRemoveItems(sectionsToRemove.flatMap(snapshot.itemIdentifiers(inSection:)))
} else if lastVisibleContentSectionIndex == contentSections.count - 1 {
let items = snapshot.itemIdentifiers(inSection: lastVisibleRowSection)
if lastVisibleRow.row < items.count - pageSize {
let itemsToRemove = Array(items.suffix(pageSize))
snapshot.deleteItems(itemsToRemove)
willRemoveItems(itemsToRemove)
} else {
return
}
} else {
return
}
dataSource.apply(snapshot, animatingDifferences: false)
}
private func loadInitial() {
guard state == .unloaded else { return }
// set loaded immediately so we don't trigger another request while the current one is running
state = .loadingInitial
loadInitialItems() { result in
guard case let .success(snapshot) = result else {
self.state = .unloaded
return
}
DispatchQueue.main.async {
self.dataSource.apply(snapshot, animatingDifferences: false)
self.state = .loaded
}
}
}
func reloadInitial() {
state = .unloaded
loadInitial()
}
func loadOlder() {
guard state != .loadingOlder else { return }
state = .loadingOlder
loadOlderItems(currentSnapshot: dataSource.snapshot()) { result in
guard case let .success(snapshot) = result else {
self.state = .loaded
return
}
DispatchQueue.main.async {
self.dataSource.apply(snapshot, animatingDifferences: false)
self.state = .loaded
}
}
}
func cellHeightChanged() {
// causes the table view to recalculate the cell heights
tableView.beginUpdates()
tableView.endUpdates()
}
// MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// this assumes that indexPathsForVisibleRows is always in order
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
let orderedContentSections = dataSource.snapshot().sectionIdentifiers.filter { timelineContentSections().contains($0) }
if indexPath.section == orderedContentSections.count - 1,
indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
loadOlder()
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
}
// MARK: - RefreshableViewController
func refresh() {
guard state != .loadingNewer else { return }
state = .loadingNewer
let snapshot = dataSource.snapshot()
var item: Item? = nil
for section in timelineContentSections() {
if let first = snapshot.itemIdentifiers(inSection: section).first {
item = first
break
}
}
loadNewerItems(currentSnapshot: snapshot) { result in
guard case let .success(snapshot) = result else {
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
self.state = .loaded
}
return
}
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
self.dataSource.apply(snapshot, animatingDifferences: false)
self.state = .loaded
if let item = item,
let indexPath = self.dataSource.indexPath(for: item) {
// maintain the current position in the list (don't scroll to top)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
// MARK: - Subclass Methods
func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
fatalError("cellProvider(_:_:_:) must be implemented by subclasses")
}
func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
}
func loadOlderItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
fatalError("loadOlderItesm(completion:) must be implemented by subclasses")
}
func loadNewerItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
fatalError("loadNewerItems(completion:) must be implemented by subclasses")
}
func timelineContentSections() -> Section.AllCases {
return Section.allCases
}
func willRemoveItems(_ items: [Item]) {
}
}
extension DiffableTimelineLikeTableViewController {
enum State: Equatable {
case unloaded
case loadingInitial
case loaded
case loadingNewer
case loadingOlder
}
}
extension DiffableTimelineLikeTableViewController {
enum LoadError: LocalizedError {
case noClient
case noOlder
case noNewer
case client(Client.Error)
}
}
extension DiffableTimelineLikeTableViewController: BackgroundableViewController {
func sceneDidEnterBackground() {
pruneOffscreenRows()
}
}

View File

@ -10,7 +10,7 @@ import UIKit
import SafariServices import SafariServices
import Pachyderm import Pachyderm
protocol MenuPreviewProvider: class { protocol MenuPreviewProvider: AnyObject {
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement]) typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
@ -37,10 +37,11 @@ extension MenuPreviewProvider {
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] } let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
guard mastodonController.loggedIn else { guard let loggedInAccountID = mastodonController.accountInfo?.id else {
// user is logged out
return [ return [
openInSafariAction(url: account.url), openInSafariAction(url: account.url),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
guard let self = self else { return } guard let self = self else { return }
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView) self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
}) })
@ -63,13 +64,15 @@ extension MenuPreviewProvider {
let request = Client.getRelationships(accounts: [account.id]) let request = Client.getRelationships(accounts: [account.id])
// talk about callback hell :/ // talk about callback hell :/
mastodonController.run(request) { [weak self] (response) in mastodonController.run(request) { [weak self] (response) in
if let self = self, guard let self = self,
case let .success(results, _) = response, case let .success(results, _) = response,
let relationship = results.first { let relationship = results.first else {
elementHandler([])
return
}
let following = relationship.following let following = relationship.following
DispatchQueue.main.async { DispatchQueue.main.async {
elementHandler([ let action = self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
let request = (following ? Account.unfollow : Account.follow)(accountID) let request = (following ? Account.unfollow : Account.follow)(accountID)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
switch response { switch response {
@ -80,21 +83,24 @@ extension MenuPreviewProvider {
} }
} }
}) })
elementHandler([
action
]) ])
} }
} }
}
})) }))
} }
let shareSection = [ var shareSection = [
openInSafariAction(url: account.url), openInSafariAction(url: account.url),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
guard let self = self else { return } guard let self = self else { return }
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView) self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
}) })
] ]
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID))
return [ return [
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
@ -104,7 +110,7 @@ extension MenuPreviewProvider {
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] { func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
return [ return [
openInSafariAction(url: url), openInSafariAction(url: url),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
guard let self = self else { return } guard let self = self else { return }
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView) self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
}) })
@ -133,13 +139,14 @@ extension MenuPreviewProvider {
] ]
} }
func actionsForStatus(_ status: StatusMO, sourceView: UIView?) -> [UIMenuElement] { func actionsForStatus(_ status: StatusMO, sourceView: UIView?, includeReply: Bool = true) -> [UIMenuElement] {
guard let mastodonController = mastodonController else { return [] } guard let mastodonController = mastodonController else { return [] }
guard mastodonController.loggedIn else { guard let accountID = mastodonController.accountInfo?.id else {
// user is logged out
return [ return [
openInSafariAction(url: status.url!), openInSafariAction(url: status.url!),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
guard let self = self else { return } guard let self = self else { return }
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView) self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
}) })
@ -150,10 +157,6 @@ extension MenuPreviewProvider {
let muted = status.muted let muted = status.muted
var actionsSection = [ var actionsSection = [
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
guard let self = self else { return }
self.navigationDelegate?.compose(inReplyToID: status.id)
}),
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
guard let self = self else { return } guard let self = self else { return }
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id) let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
@ -174,9 +177,16 @@ extension MenuPreviewProvider {
}) })
] ]
if includeReply {
actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
guard let self = self else { return }
self.navigationDelegate?.compose(inReplyToID: status.id)
}), at: 0)
}
if mastodonController.account != nil && mastodonController.account.id == status.account.id { if mastodonController.account != nil && mastodonController.account.id == status.account.id {
let pinned = status.pinned ?? false let pinned = status.pinned ?? false
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin" : "Pin", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
guard let self = self else { return } guard let self = self else { return }
let request = (pinned ? Status.unpin : Status.pin)(status.id) let request = (pinned ? Status.unpin : Status.pin)(status.id)
self.mastodonController?.run(request, completion: { [weak self] (response) in self.mastodonController?.run(request, completion: { [weak self] (response) in
@ -204,21 +214,13 @@ extension MenuPreviewProvider {
var shareSection = [ var shareSection = [
openInSafariAction(url: status.url!), openInSafariAction(url: status.url!),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
guard let self = self else { return } guard let self = self else { return }
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView) self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
}), }),
] ]
#if targetEnvironment(macCatalyst) addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
shareSection.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "", handler: { (_) in
guard let id = mastodonController.accountInfo?.id else {
return
}
// todo: this should try to find an existing session
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: id), options: nil, errorHandler: nil)
}))
#endif
return [ return [
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
@ -242,6 +244,28 @@ extension MenuPreviewProvider {
}) })
} }
private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
#if SDK_IOS_15
if #available(iOS 15.0, *) {
let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .automatic
actions.append(UIWindowScene.ActivationAction { (_) in
return .init(userActivity: activity(), options: options, preview: nil)
})
} else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
}))
}
#else
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
}))
}
#endif
}
} }
extension LargeImageViewController: CustomPreviewPresenting { extension LargeImageViewController: CustomPreviewPresenting {

View File

@ -44,6 +44,13 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
addKeyCommand(MenuController.prevSubTabCommand) addKeyCommand(MenuController.prevSubTabCommand)
addKeyCommand(MenuController.nextSubTabCommand) addKeyCommand(MenuController.nextSubTabCommand)
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
if let nav = navigationController {
let appearance = UINavigationBarAppearance()
appearance.configureWithDefaultBackground()
nav.navigationBar.scrollEdgeAppearance = appearance
}
} }
func selectPage(at index: Int, animated: Bool) { func selectPage(at index: Int, animated: Bool) {

View File

@ -9,8 +9,8 @@
import UIKit import UIKit
/// A table view controller that manages common functionality between timeline-like UIs. /// A table view controller that manages common functionality between timeline-like UIs.
// For example, this class handles loading new items when the user scrolls to the end, /// For example, this class handles loading new items when the user scrolls to the end,
// refreshing, and pruning offscreen rows automatically. /// refreshing, and pruning offscreen rows automatically.
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController { class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
private(set) var loaded = false private(set) var loaded = false

View File

@ -38,6 +38,15 @@ class UserActivityManager {
return LocalData.shared.getAccount(id: id) return LocalData.shared.getAccount(id: id)
} }
// MARK: - Main Scene
static func mainSceneActivity(accountID: String) -> NSUserActivity {
let activity = NSUserActivity(type: .mainScene)
activity.userInfo = [
"accountID": accountID,
]
return activity
}
// MARK: - New Post // MARK: - New Post
static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity { static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity {
// todo: update to use managed objects // todo: update to use managed objects
@ -61,6 +70,7 @@ class UserActivityManager {
// TODO: check not currently showing compose screen // TODO: check not currently showing compose screen
let mentioning = activity.userInfo?["mentioning"] as? String let mentioning = activity.userInfo?["mentioning"] as? String
let draft = mastodonController.createDraft(mentioningAcct: mentioning) let draft = mastodonController.createDraft(mentioningAcct: mentioning)
// todo: this shouldn't use self.mastodonController, it should get the right one based on the userInfo accountID
let composeVC = ComposeHostingController(draft: draft, mastodonController: mastodonController) let composeVC = ComposeHostingController(draft: draft, mastodonController: mastodonController)
present(UINavigationController(rootViewController: composeVC)) present(UINavigationController(rootViewController: composeVC))
} }

View File

@ -9,6 +9,7 @@
import Foundation import Foundation
enum UserActivityType: String { enum UserActivityType: String {
case mainScene = "space.vaccor.Tusker.activity.main-scene"
case newPost = "space.vaccor.Tusker.activity.new-post" case newPost = "space.vaccor.Tusker.activity.new-post"
case checkNotifications = "space.vaccor.Tusker.activity.check-notifications" case checkNotifications = "space.vaccor.Tusker.activity.check-notifications"
case showTimeline = "space.vaccor.Tusker.activity.show-timeline" case showTimeline = "space.vaccor.Tusker.activity.show-timeline"
@ -22,6 +23,8 @@ enum UserActivityType: String {
extension UserActivityType { extension UserActivityType {
var handle: (NSUserActivity) -> Void { var handle: (NSUserActivity) -> Void {
switch self { switch self {
case .mainScene:
fatalError("cannot handle main scene activity")
case .newPost: case .newPost:
return UserActivityManager.handleNewPost return UserActivityManager.handleNewPost
case .checkNotifications: case .checkNotifications:

View File

@ -89,15 +89,22 @@ extension TuskerNavigationDelegate {
} }
func compose(editing draft: Draft) { func compose(editing draft: Draft) {
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
} else {
let compose = ComposeHostingController(draft: draft, mastodonController: apiController) let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
let nav = UINavigationController(rootViewController: compose)
let vc = UINavigationController(rootViewController: compose) nav.presentationController?.delegate = compose
vc.presentationController?.delegate = compose present(nav, animated: true)
present(vc, animated: true) }
} }
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) { func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct) let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
DraftsManager.shared.add(draft)
compose(editing: draft) compose(editing: draft)
} }

View File

@ -11,7 +11,7 @@ import Pachyderm
import Gifu import Gifu
import AVFoundation import AVFoundation
protocol AttachmentViewDelegate: class { protocol AttachmentViewDelegate: AnyObject {
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
} }
@ -75,6 +75,9 @@ class AttachmentView: UIImageView, GIFAnimatable {
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil)
addInteraction(UIContextMenuInteraction(delegate: self)) addInteraction(UIContextMenuInteraction(delegate: self))
isAccessibilityElement = true
accessibilityTraits = [.image, .button]
} }
@objc private func preferencesChanged() { @objc private func preferencesChanged() {
@ -161,7 +164,9 @@ class AttachmentView: UIImageView, GIFAnimatable {
let attachmentURL = attachment.url let attachmentURL = attachment.url
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
guard let self = self, let data = data else { return } guard let self = self, let data = data else { return }
DispatchQueue.main.async {
self.attachmentRequest = nil self.attachmentRequest = nil
}
if self.attachment.url.pathExtension == "gif" { if self.attachment.url.pathExtension == "gif" {
self.source = .gifData(attachmentURL, data) self.source = .gifData(attachmentURL, data)
if self.autoplayGifs { if self.autoplayGifs {
@ -294,6 +299,13 @@ class AttachmentView: UIImageView, GIFAnimatable {
showGallery() showGallery()
} }
// MARK: - Accessibility
override func accessibilityActivate() -> Bool {
showGallery()
return true
}
} }
fileprivate extension AttachmentView { fileprivate extension AttachmentView {

View File

@ -248,9 +248,11 @@ class AttachmentsContainerView: UIView {
let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size) let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size)
attachmentView.delegate = delegate attachmentView.delegate = delegate
attachmentView.translatesAutoresizingMaskIntoConstraints = false attachmentView.translatesAutoresizingMaskIntoConstraints = false
attachmentView.isAccessibilityElement = true
attachmentView.accessibilityTraits = [.image, .button]
attachmentView.accessibilityLabel = String(format: NSLocalizedString("Attachment %d", comment: "attachment at index accessiblity label"), index + 1) attachmentView.accessibilityLabel = String(format: NSLocalizedString("Attachment %d", comment: "attachment at index accessiblity label"), index + 1)
attachmentView.accessibilityLabel = "Attachment \(index + 1)"
if let desc = attachments[index].description {
attachmentView.accessibilityLabel! += ", \(desc)"
}
attachmentViews.add(attachmentView) attachmentViews.add(attachmentView)
return attachmentView return attachmentView
} }

View File

@ -11,7 +11,7 @@ import Pachyderm
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
protocol BaseEmojiLabel: class { protocol BaseEmojiLabel: AnyObject {
var emojiIdentifier: String? { get set } var emojiIdentifier: String? { get set }
var emojiRequests: [ImageCache.Request] { get set } var emojiRequests: [ImageCache.Request] { get set }
var emojiFont: UIFont { get } var emojiFont: UIFont { get }

View File

@ -0,0 +1,55 @@
//
// ConfirmLoadMoreTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 6/23/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
protocol ConfirmLoadOlderTableViewCellDelegate: AnyObject {
func confirmLoadMore()
}
class ConfirmLoadMoreTableViewCell: UITableViewCell {
var confirmLoadMore: (() -> Void)?
@IBOutlet weak var confirmButton: UIButton!
private var isLoading = false
override func awakeFromNib() {
super.awakeFromNib()
if #available(iOS 15.0, *) {
var config = UIButton.Configuration.tinted()
config.title = "Load More"
config.showsActivityIndicator = false
config.imagePadding = 4
confirmButton.configuration = config
confirmButton.configurationUpdateHandler = { [unowned self] button in
button.configuration?.showsActivityIndicator = self.isLoading
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
isLoading = false
if #available(iOS 15.0, *) {
confirmButton.setNeedsUpdateConfiguration()
}
}
@IBAction func loadMorePressed(_ sender: Any) {
confirmLoadMore?()
if #available(iOS 15.0, *) {
isLoading = true
confirmButton.setNeedsUpdateConfiguration()
}
}
}

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19115.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19107.5"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="105" id="KGk-i7-Jjw" customClass="ConfirmLoadMoreTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="105"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="105"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="Rpx-45-c2n">
<rect key="frame" x="16" y="11" width="288" height="86"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Infinite scrolling is off. Do you want to keep going?" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9nv-Re-5sL">
<rect key="frame" x="0.0" y="0.0" width="288" height="41"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NT9-ly-efr">
<rect key="frame" x="0.0" y="45" width="288" height="41"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="tinted" title="Load More"/>
<connections>
<action selector="loadMorePressed:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="Pgz-MB-icB"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
<constraints>
<constraint firstItem="Rpx-45-c2n" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="YfZ-rr-Omf"/>
<constraint firstItem="Rpx-45-c2n" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="hhi-yX-Wa4"/>
<constraint firstAttribute="trailingMargin" secondItem="Rpx-45-c2n" secondAttribute="trailing" id="jI8-St-34M"/>
<constraint firstAttribute="bottom" secondItem="Rpx-45-c2n" secondAttribute="bottom" constant="8" id="mQh-0l-Eo2"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="confirmButton" destination="NT9-ly-efr" id="Lja-th-LeH"/>
</connections>
<point key="canvasLocation" x="131.8840579710145" y="150.33482142857142"/>
</tableViewCell>
</objects>
<resources>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="secondarySystemBackgroundColor">
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@ -0,0 +1,30 @@
//
// FastAccountSwitchingIndicatorView.swift
// Tusker
//
// Created by Shadowfacts on 6/6/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
class FastAccountSwitcherIndicatorView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
tintColor = .lightGray
let up = UIImageView(image: UIImage(systemName: "arrowtriangle.up.fill")!)
up.frame = CGRect(x: 0, y: 0, width: 10, height: 5)
addSubview(up)
let down = UIImageView(image: UIImage(systemName: "arrowtriangle.down.fill")!)
down.frame = CGRect(x: 0, y: 7, width: 10, height: 5)
addSubview(down)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -30,6 +30,7 @@ class PollOptionView: UIView {
let label = EmojiLabel() let label = EmojiLabel()
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0 label.numberOfLines = 0
label.font = .preferredFont(forTextStyle: .callout)
label.text = option.title label.text = option.title
label.setEmojis(poll.emojis, identifier: poll.id) label.setEmojis(poll.emojis, identifier: poll.id)
addSubview(label) addSubview(label)
@ -37,15 +38,21 @@ class PollOptionView: UIView {
let percentLabel = UILabel() let percentLabel = UILabel()
percentLabel.translatesAutoresizingMaskIntoConstraints = false percentLabel.translatesAutoresizingMaskIntoConstraints = false
percentLabel.text = "100%" percentLabel.text = "100%"
percentLabel.font = label.font
percentLabel.isHidden = true percentLabel.isHidden = true
percentLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
percentLabel.setContentHuggingPriority(.required, for: .horizontal)
addSubview(percentLabel) addSubview(percentLabel)
accessibilityLabel = option.title
if (poll.voted ?? false) || poll.effectiveExpired, if (poll.voted ?? false) || poll.effectiveExpired,
let optionVotes = option.votesCount { let optionVotes = option.votesCount {
let frac = poll.votesCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(poll.votesCount) let frac = poll.votesCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(poll.votesCount)
let percent = String(format: "%.0f%%", frac * 100)
percentLabel.isHidden = false percentLabel.isHidden = false
percentLabel.text = String(format: "%.0f%%", frac * 100) percentLabel.text = percent
let fillView = UIView() let fillView = UIView()
fillView.translatesAutoresizingMaskIntoConstraints = false fillView.translatesAutoresizingMaskIntoConstraints = false
@ -60,6 +67,8 @@ class PollOptionView: UIView {
fillView.topAnchor.constraint(equalTo: topAnchor), fillView.topAnchor.constraint(equalTo: topAnchor),
fillView.bottomAnchor.constraint(equalTo: bottomAnchor), fillView.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
accessibilityLabel! += ", \(percent)"
} }
let minHeightConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight) let minHeightConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
@ -76,12 +85,14 @@ class PollOptionView: UIView {
label.topAnchor.constraint(equalTo: topAnchor), label.topAnchor.constraint(equalTo: topAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor), label.bottomAnchor.constraint(equalTo: bottomAnchor),
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8), label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor), label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4),
percentLabel.topAnchor.constraint(equalTo: topAnchor), percentLabel.topAnchor.constraint(equalTo: topAnchor),
percentLabel.bottomAnchor.constraint(equalTo: bottomAnchor), percentLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
]) ])
isAccessibilityElement = true
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {

View File

@ -69,6 +69,8 @@ class PollOptionsView: UIControl {
stack.addArrangedSubview(optionView) stack.addArrangedSubview(optionView)
return optionView return optionView
} }
accessibilityElements = options
} }
private func selectOption(_ option: PollOptionView) { private func selectOption(_ option: PollOptionView) {

View File

@ -23,17 +23,20 @@ class StatusPollView: UIView {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private var statusID: String! private var statusID: String!
private var poll: Poll! private(set) var poll: Poll?
private var optionsView: PollOptionsView! private var optionsView: PollOptionsView!
private var voteButton: UIButton! private var voteButton: UIButton!
private var infoLabel: UILabel! private var infoLabel: UILabel!
private var options: [PollOptionView] = []
private var canVote = true private var canVote = true
private var animator: UIViewPropertyAnimator! private var animator: UIViewPropertyAnimator!
private var currentSelectedOptionIndex: Int! private var currentSelectedOptionIndex: Int!
var isTracking: Bool {
optionsView.isTracking
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -74,13 +77,15 @@ class StatusPollView: UIView {
voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor), voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
voteButton.bottomAnchor.constraint(equalTo: bottomAnchor), voteButton.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
accessibilityElements = [optionsView!, infoLabel!, voteButton!]
} }
func updateUI(status: StatusMO, poll: Poll) { func updateUI(status: StatusMO, poll: Poll?) {
self.statusID = status.id self.statusID = status.id
self.poll = poll self.poll = poll
options.forEach { $0.removeFromSuperview() } guard let poll = poll else { return }
// poll.voted is nil if there is no user (e.g., public timeline), in which case the poll also cannot be voted upon // poll.voted is nil if there is no user (e.g., public timeline), in which case the poll also cannot be voted upon
if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account.id { if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account.id {
@ -138,7 +143,7 @@ class StatusPollView: UIView {
UIImpactFeedbackGenerator(style: .medium).impactOccurred() UIImpactFeedbackGenerator(style: .medium).impactOccurred()
let request = Poll.vote(poll.id, choices: optionsView.checkedOptionIndices) let request = Poll.vote(poll!.id, choices: optionsView.checkedOptionIndices)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
switch response { switch response {
case let .failure(error): case let .failure(error):

View File

@ -12,8 +12,6 @@ import Combine
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate { protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate {
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int)
func profileHeader(_ headerView: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView)
} }
class ProfileHeaderView: UIView { class ProfileHeaderView: UIView {
@ -82,14 +80,14 @@ class ProfileHeaderView: UIView {
cancellables = [] cancellables = []
mastodonController.persistentContainer.accountSubject mastodonController.persistentContainer.accountSubject
.filter { [weak self] in $0 == self?.accountID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.filter { [weak self] in $0 == self?.accountID }
.sink { [weak self] in self?.updateUI(for: $0) } .sink { [weak self] in self?.updateUI(for: $0) }
.store(in: &cancellables) .store(in: &cancellables)
mastodonController.persistentContainer.relationshipSubject mastodonController.persistentContainer.relationshipSubject
.filter { [weak self] in $0 == self?.accountID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.filter { [weak self] in $0 == self?.accountID }
.sink { [weak self] (_) in self?.updateRelationship() } .sink { [weak self] (_) in self?.updateRelationship() }
.store(in: &cancellables) .store(in: &cancellables)
} }
@ -134,6 +132,7 @@ class ProfileHeaderView: UIView {
fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
var fieldAccessibilityElements = [Any]()
for field in account.fields { for field in account.fields {
let nameLabel = EmojiLabel() let nameLabel = EmojiLabel()
nameLabel.text = field.name nameLabel.text = field.name
@ -157,7 +156,18 @@ class ProfileHeaderView: UIView {
fieldValuesStackView.addArrangedSubview(valueTextView) fieldValuesStackView.addArrangedSubview(valueTextView)
nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true
fieldAccessibilityElements.append(nameLabel)
fieldAccessibilityElements.append(valueTextView)
} }
accessibilityElements = [
displayNameLabel!,
usernameLabel!,
noteTextView!,
] + fieldAccessibilityElements + [
moreButton!,
pagesSegmentedControl!,
]
} }
private func updateRelationship() { private func updateRelationship() {
@ -193,11 +203,14 @@ class ProfileHeaderView: UIView {
let image = image, let image = image,
self.accountID == accountID, self.accountID == accountID,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
DispatchQueue.main.async {
self?.avatarRequest = nil
}
return return
} }
self.avatarRequest = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarRequest = nil
self.avatarImageView.image = transformedImage self.avatarImageView.image = transformedImage
} }
} }
@ -207,11 +220,14 @@ class ProfileHeaderView: UIView {
let image = image, let image = image,
self.accountID == accountID, self.accountID == accountID,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
DispatchQueue.main.async {
self?.headerRequest = nil
}
return return
} }
self.headerRequest = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.headerRequest = nil
self.headerImageView.image = transformedImage self.headerImageView.image = transformedImage
} }
} }

View File

@ -94,8 +94,7 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
collapseButton.layer.masksToBounds = true collapseButton.layer.masksToBounds = true
collapseButton.layer.cornerRadius = 5 collapseButton.layer.cornerRadius = 5
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!] accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!, pollView!]
attachmentsView.isAccessibilityElement = true
moreButton.showsMenuAsPrimaryAction = true moreButton.showsMenuAsPrimaryAction = true
@ -107,8 +106,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
open func createObserversIfNecessary() { open func createObserversIfNecessary() {
if statusUpdater == nil { if statusUpdater == nil {
statusUpdater = mastodonController.persistentContainer.statusSubject statusUpdater = mastodonController.persistentContainer.statusSubject
.filter { [unowned self] in $0 == self.statusID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.filter { [unowned self] in $0 == self.statusID }
.sink { [unowned self] in .sink { [unowned self] in
if let mastodonController = mastodonController, if let mastodonController = mastodonController,
let status = mastodonController.persistentContainer.status(for: $0) { let status = mastodonController.persistentContainer.status(for: $0) {
@ -119,8 +118,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
if accountUpdater == nil { if accountUpdater == nil {
accountUpdater = mastodonController.persistentContainer.accountSubject accountUpdater = mastodonController.persistentContainer.accountSubject
.filter { [unowned self] in $0 == self.accountID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.filter { [unowned self] in $0 == self.accountID }
.sink { [unowned self] in .sink { [unowned self] in
if let mastodonController = mastodonController, if let mastodonController = mastodonController,
let account = mastodonController.persistentContainer.account(for: $0) { let account = mastodonController.persistentContainer.account(for: $0) {
@ -157,8 +156,6 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
cardView.navigationDelegate = navigationDelegate cardView.navigationDelegate = navigationDelegate
attachmentsView.updateUI(status: status) attachmentsView.updateUI(status: status)
attachmentsView.isAccessibilityElement = status.attachments.count > 0
attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count)
updateStatusState(status: status) updateStatusState(status: status)
@ -213,15 +210,12 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
} }
// keep menu in sync with changed states e.g. bookmarked, muted // keep menu in sync with changed states e.g. bookmarked, muted
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton)) // do not include reply action here, because the cell already contains a button for it
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton, includeReply: false))
if let poll = status.poll { pollView.isHidden = status.poll == nil
pollView.isHidden = false
pollView.mastodonController = mastodonController pollView.mastodonController = mastodonController
pollView.updateUI(status: status, poll: poll) pollView.updateUI(status: status, poll: status.poll)
} else {
pollView.isHidden = true
}
} }
func updateUI(account: AccountMO) { func updateUI(account: AccountMO) {
@ -359,6 +353,7 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
contentTextView.isHidden = collapsed contentTextView.isHidden = collapsed
cardView.isHidden = cardView.card == nil || collapsed cardView.isHidden = cardView.card == nil || collapsed
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
pollView.isHidden = pollView.poll == nil || collapsed
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")! let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!

View File

@ -33,7 +33,21 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self) profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self) profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self)
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentTextView!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!] accessibilityElements = [
profileAccessibilityElement!,
contentWarningLabel!,
collapseButton!,
contentTextView!,
attachmentsView!,
pollView!,
totalFavoritesButton!,
totalReblogsButton!,
timestampAndClientLabel!,
replyButton!,
favoriteButton!,
reblogButton!,
moreButton!,
]
contentTextView.defaultFont = .systemFont(ofSize: 18) contentTextView.defaultFont = .systemFont(ofSize: 18)

View File

@ -15,7 +15,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
static let relativeDateFormatter: RelativeDateTimeFormatter = { static let relativeDateFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter() let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .numeric formatter.dateTimeStyle = .numeric
formatter.unitsStyle = .short formatter.unitsStyle = .full
return formatter return formatter
}() }()
@ -44,7 +44,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed))) reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
accessibilityElements!.insert(reblogLabel!, at: 0) isAccessibilityElement = true
// todo: double check this on RTL layouts // todo: double check this on RTL layouts
replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
@ -59,8 +59,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
if rebloggerAccountUpdater == nil { if rebloggerAccountUpdater == nil {
rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject
.filter { [unowned self] in $0 == self.rebloggerID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.filter { [unowned self] in $0 == self.rebloggerID }
.sink { [unowned self] in .sink { [unowned self] in
if let mastodonController = self.mastodonController, if let mastodonController = self.mastodonController,
let reblogger = mastodonController.persistentContainer.account(for: $0) { let reblogger = mastodonController.persistentContainer.account(for: $0) {
@ -134,7 +134,6 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
private func doUpdateTimestamp(status: StatusMO) { private func doUpdateTimestamp(status: StatusMO) {
timestampLabel.text = status.createdAt.timeAgoString() timestampLabel.text = status.createdAt.timeAgoString()
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
let delay: DispatchTimeInterval? let delay: DispatchTimeInterval?
switch status.createdAt.timeAgo().1 { switch status.createdAt.timeAgo().1 {
@ -194,6 +193,38 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
) )
} }
// MARK: - Accessibility
override var accessibilityLabel: String? {
get {
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
return nil
}
var str = "\(status.account.displayName), \(contentTextView.text ?? "")"
if status.attachments.count > 0 {
// todo: localize me
str += ", \(status.attachments.count) attachments"
}
if status.poll != nil {
str += ", poll"
}
str += ", \(TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date()))"
if let rebloggerID = rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
str += ", reblogged by \(reblogger.displayName)"
}
return str
}
set {}
}
override func accessibilityActivate() -> Bool {
didSelectCell()
return true
}
} }
extension TimelineStatusTableViewCell: SelectableTableViewCell { extension TimelineStatusTableViewCell: SelectableTableViewCell {
@ -295,8 +326,11 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
extension TimelineStatusTableViewCell: DraggableTableViewCell { extension TimelineStatusTableViewCell: DraggableTableViewCell {
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] { func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
// the poll options view is tracking while the user is dragging between options
// while that's happening, don't initiate a drag
guard let status = mastodonController.persistentContainer.status(for: statusID), guard let status = mastodonController.persistentContainer.status(for: statusID),
let accountID = mastodonController.accountInfo?.id else { let accountID = mastodonController.accountInfo?.id,
!pollView.isTracking else {
return [] return []
} }
let provider = NSItemProvider(object: status.url! as NSURL) let provider = NSItemProvider(object: status.url! as NSURL)
@ -314,4 +348,19 @@ extension TimelineStatusTableViewCell: UIContextMenuInteractionDelegate {
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView)) return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView))
} }
} }
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
if let viewController = animator.previewViewController,
let delegate = navigationDelegate {
animator.preferredCommitStyle = .pop
animator.addCompletion {
if let customPresenting = viewController as? CustomPreviewPresenting {
customPresenting.presentFromPreview(presenter: delegate)
} else {
delegate.show(viewController)
}
}
}
}
} }

View File

@ -1,8 +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="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19115.2" 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>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19107.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"/>
@ -38,7 +39,7 @@
</constraints> </constraints>
</imageView> </imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk"> <stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
<rect key="frame" x="58" y="0.0" width="277" height="169.5"/> <rect key="frame" x="58" y="0.0" width="277" height="173.5"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf"> <stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
<rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/>
@ -132,13 +133,19 @@
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/> <rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view> </view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="1" verticalCompressionResistancePriority="1" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oFl-rC-EEN">
<rect key="frame" x="0.0" y="173.5" width="277" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU"> <stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
<rect key="frame" x="0.0" y="54" width="50" height="22"/> <rect key="frame" x="0.0" y="54" width="50" height="22"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
<rect key="frame" x="0.0" y="1" width="25.5" height="21.5"/> <rect key="frame" x="0.0" y="0.0" width="25.5" height="21.5"/>
<color key="tintColor" systemColor="secondaryLabelColor"/> <color key="tintColor" systemColor="secondaryLabelColor"/>
<accessibility key="accessibilityConfiguration" label="Is a reply"/> <accessibility key="accessibilityConfiguration" label="Is a reply"/>
<constraints> <constraints>
@ -218,7 +225,7 @@
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/> <constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/> <constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="trailing" secondItem="ve3-Y1-NQH" secondAttribute="trailingMargin" id="3l0-tE-Ak1"/> <constraint firstItem="TUP-Nz-5Yh" firstAttribute="trailing" secondItem="ve3-Y1-NQH" secondAttribute="trailingMargin" id="3l0-tE-Ak1"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="top" secondItem="gIY-Wp-RSk" secondAttribute="bottom" id="4KL-a3-qyf"/> <constraint firstItem="TUP-Nz-5Yh" firstAttribute="top" secondItem="gIY-Wp-RSk" secondAttribute="bottom" constant="-4" id="4KL-a3-qyf"/>
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/> <constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="oie-wK-IpU" secondAttribute="bottom" id="7Xp-Sa-Rfk"/> <constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="oie-wK-IpU" secondAttribute="bottom" id="7Xp-Sa-Rfk"/>
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/> <constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
@ -276,7 +283,7 @@
<image name="ellipsis" catalog="system" width="128" height="37"/> <image name="ellipsis" catalog="system" width="128" height="37"/>
<image name="globe" catalog="system" width="128" height="121"/> <image name="globe" catalog="system" width="128" height="121"/>
<image name="pin.fill" catalog="system" width="119" height="128"/> <image name="pin.fill" catalog="system" width="119" height="128"/>
<image name="repeat" catalog="system" width="128" height="99"/> <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 white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@ -55,6 +55,8 @@ class VisualEffectImageButton: UIControl {
addInteraction(UIContextMenuInteraction(delegate: self)) addInteraction(UIContextMenuInteraction(delegate: self))
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap))) addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
isAccessibilityElement = true
} }
@objc private func onTap() { @objc private func onTap() {