Compare commits
53 Commits
e7e141bd1e
...
9b30b48016
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 9b30b48016 | |
Shadowfacts | bd49683e13 | |
Shadowfacts | c22945b1e7 | |
Shadowfacts | 0a16a2e261 | |
Shadowfacts | b95819cada | |
Shadowfacts | dc1ea1bed9 | |
Shadowfacts | 5f9fe505d5 | |
Shadowfacts | 5b8e97287e | |
Shadowfacts | 49572c1fec | |
Shadowfacts | ebb0770198 | |
Shadowfacts | 27e05cc72d | |
Shadowfacts | 4ca48a5f50 | |
Shadowfacts | 230bd50661 | |
Shadowfacts | 4f2f8d517f | |
Shadowfacts | 130da9d4cc | |
Shadowfacts | 472b9aa5e2 | |
Shadowfacts | 3413dff8f9 | |
Shadowfacts | 66e8fce488 | |
Shadowfacts | aa2d333f4a | |
Shadowfacts | c8a45d8eef | |
Shadowfacts | 40f5be28f6 | |
Shadowfacts | 7c9287543c | |
Shadowfacts | 2a05b6d326 | |
Shadowfacts | 2499d25432 | |
Shadowfacts | 9417872790 | |
Shadowfacts | c02a1bbf74 | |
Shadowfacts | 0a894b219a | |
Shadowfacts | 22803668d2 | |
Shadowfacts | 2f6d1cb069 | |
Shadowfacts | 8889261b6b | |
Shadowfacts | 91f1a5195c | |
Shadowfacts | 1a5b958b1a | |
Shadowfacts | d667f6362c | |
Shadowfacts | ef1db466b9 | |
Shadowfacts | 0566f0ddfa | |
Shadowfacts | f54d4d757f | |
Shadowfacts | fbc5d6eed9 | |
Shadowfacts | 2c4d2ce551 | |
Shadowfacts | bbe260bc9e | |
Shadowfacts | 2fe19a5abe | |
Shadowfacts | feacf576d7 | |
Shadowfacts | ceb58f1d92 | |
Shadowfacts | 806591f5b7 | |
Shadowfacts | 18ce21c2c6 | |
Shadowfacts | 47fb0ea868 | |
Shadowfacts | ffe6450b26 | |
Shadowfacts | b51c1c03cb | |
Shadowfacts | e745d78d67 | |
Shadowfacts | 4c9d5e8465 | |
Shadowfacts | 9ec7177bfa | |
Shadowfacts | 421881d461 | |
Shadowfacts | c78f152670 | |
Shadowfacts | dabcae0905 |
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -1,5 +1,40 @@
|
|||
# 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)
|
||||
This is an emergency fix for Tusker breaking when connecting to Mastodon instances on 3.4.0rc1.
|
||||
|
||||
|
|
|
@ -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();
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"color" : {
|
||||
"reference" : "systemPurpleColor"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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>
|
|
@ -155,6 +155,7 @@
|
|||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.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 */; };
|
||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.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 */; };
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.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 */; };
|
||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.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 */; };
|
||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.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 */; };
|
||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.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 */; };
|
||||
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
|
||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
||||
|
@ -363,9 +372,27 @@
|
|||
remoteGlobalIDString = D6D4DDCB212518A000E1C4BB;
|
||||
remoteInfo = Tusker;
|
||||
};
|
||||
D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D6E343A7265AAD6B00C4AA01;
|
||||
remoteInfo = OpenInTusker;
|
||||
};
|
||||
/* End PBXContainerItemProxy 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 */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -388,7 +415,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -530,6 +556,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
|
@ -565,6 +592,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -683,10 +711,19 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -752,6 +789,13 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D6E343A5265AAD6B00C4AA01 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
|
@ -1413,6 +1457,7 @@
|
|||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||
D626494023C122C800612E6E /* Asset Picker */,
|
||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||
|
@ -1424,6 +1469,7 @@
|
|||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
|
||||
D61AC1DA232EA43100C54D2D /* Instance Cell */,
|
||||
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1446,6 +1492,7 @@
|
|||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
||||
);
|
||||
|
@ -1466,12 +1513,12 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6BC874421961F73006163F1 /* Gifu.framework */,
|
||||
0461A38F2163CBAE00C0A807 /* Cache.framework */,
|
||||
D61099AC2144B0CC00432DC2 /* Pachyderm */,
|
||||
D61099B92144B0CC00432DC2 /* PachydermTests */,
|
||||
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
||||
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
||||
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
||||
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */,
|
||||
D6D4DDCD212518A000E1C4BB /* Products */,
|
||||
D65A37F221472F300087646E /* Frameworks */,
|
||||
);
|
||||
|
@ -1485,6 +1532,7 @@
|
|||
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */,
|
||||
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */,
|
||||
D61099B32144B0CC00432DC2 /* PachydermTests.xctest */,
|
||||
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1552,6 +1600,28 @@
|
|||
path = TuskerUITests;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1642,11 +1712,13 @@
|
|||
D6D4DDCA212518A000E1C4BB /* Resources */,
|
||||
D6F953E52125197500CF0F2B /* Embed Frameworks */,
|
||||
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
|
||||
D6E3438F2659849800C4AA01 /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
D61099BF2144B0CC00432DC2 /* PBXTargetDependency */,
|
||||
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Tusker;
|
||||
packageProductDependencies = (
|
||||
|
@ -1696,14 +1768,31 @@
|
|||
productReference = D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */;
|
||||
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 */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
D6D4DDC4212518A000E1C4BB /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1200;
|
||||
LastUpgradeCheck = 1020;
|
||||
LastSwiftUpdateCheck = 1250;
|
||||
LastUpgradeCheck = 1250;
|
||||
ORGANIZATIONNAME = Shadowfacts;
|
||||
TargetAttributes = {
|
||||
D61099AA2144B0CC00432DC2 = {
|
||||
|
@ -1729,6 +1818,9 @@
|
|||
LastSwiftMigration = 1020;
|
||||
TestTargetID = D6D4DDCB212518A000E1C4BB;
|
||||
};
|
||||
D6E343A7265AAD6B00C4AA01 = {
|
||||
CreatedOnToolsVersion = 12.5;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = D6D4DDC7212518A000E1C4BB /* Build configuration list for PBXProject "Tusker" */;
|
||||
|
@ -1760,6 +1852,7 @@
|
|||
D6D4DDEA212518A200E1C4BB /* TuskerUITests */,
|
||||
D61099AA2144B0CC00432DC2 /* Pachyderm */,
|
||||
D61099B22144B0CC00432DC2 /* PachydermTests */,
|
||||
D6E343A7265AAD6B00C4AA01 /* OpenInTusker */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
@ -1810,6 +1903,7 @@
|
|||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
|
||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
||||
D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1827,6 +1921,16 @@
|
|||
);
|
||||
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 */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
|
@ -1933,6 +2037,7 @@
|
|||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
|
||||
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */,
|
||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||
|
@ -2065,6 +2170,7 @@
|
|||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
|
||||
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||
|
@ -2085,6 +2191,7 @@
|
|||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
|
||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||
|
@ -2170,6 +2277,14 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D6E343A4265AAD6B00C4AA01 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
|
@ -2198,6 +2313,11 @@
|
|||
target = D6D4DDCB212518A000E1C4BB /* Tusker */;
|
||||
targetProxy = D6D4DDEC212518A200E1C4BB /* PBXContainerItemProxy */;
|
||||
};
|
||||
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */;
|
||||
targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
|
@ -2209,6 +2329,14 @@
|
|||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
D6E343AF265AAD6B00C4AA01 /* Base */,
|
||||
);
|
||||
name = MainInterface.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
|
@ -2234,6 +2362,7 @@
|
|||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Pachyderm/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2264,6 +2393,7 @@
|
|||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Pachyderm/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2346,6 +2476,7 @@
|
|||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
|
@ -2408,6 +2539,7 @@
|
|||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
|
@ -2445,11 +2577,11 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2461,6 +2593,10 @@
|
|||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
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;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
};
|
||||
|
@ -2474,11 +2610,11 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2576,6 +2712,60 @@
|
|||
};
|
||||
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 */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
|
@ -2633,6 +2823,15 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D6E343B6265AAD6B00C4AA01 /* Build configuration list for PBXNativeTarget "OpenInTusker" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D6E343B7265AAD6B00C4AA01 /* Debug */,
|
||||
D6E343B8265AAD6B00C4AA01 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
|
@ -2649,7 +2848,7 @@
|
|||
repositoryURL = "https://github.com/microsoft/plcrashreporter";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 1.7.0;
|
||||
minimumVersion = 1.8.0;
|
||||
};
|
||||
};
|
||||
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -29,8 +29,6 @@
|
|||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
@ -51,8 +49,6 @@
|
|||
ReferencedContainer = "container:Tusker.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -88,6 +88,10 @@
|
|||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-UIFocusLoopDebuggerEnabled YES"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "6b7ca9a2faad6ea990ff60b0a3ee4fdf3db59150",
|
||||
"version": "1.7.2"
|
||||
"revision": "de6b8f9db4b2a0aa859a5507550a70548e4da936",
|
||||
"version": "1.8.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -65,6 +65,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
}
|
||||
|
||||
switch type {
|
||||
case .mainScene:
|
||||
return "main-scene"
|
||||
|
||||
case .showConversation,
|
||||
.showTimeline,
|
||||
.checkNotifications,
|
||||
|
@ -76,9 +79,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
case .newPost:
|
||||
return "compose"
|
||||
|
||||
default:
|
||||
fatalError("no scene for activity type \(type)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,21 +17,39 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
guard LocalData.shared.onboardingComplete else {
|
||||
UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let account: LocalData.UserAccountInfo
|
||||
let controller: MastodonController
|
||||
let draft: Draft?
|
||||
|
||||
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity,
|
||||
let activityAccount = UserActivityManager.getAccount(from: activity) {
|
||||
account = activityAccount
|
||||
draft = UserActivityManager.getDraft(from: activity)
|
||||
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||
if let activityAccount = UserActivityManager.getAccount(from: activity) {
|
||||
account = activityAccount
|
||||
} 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 {
|
||||
account = LocalData.shared.getMostRecentAccount()!
|
||||
controller = MastodonController.getForAccount(account)
|
||||
draft = nil
|
||||
}
|
||||
|
||||
let controller = MastodonController.getForAccount(account)
|
||||
session.mastodonController = controller
|
||||
|
||||
controller.getOwnAccount()
|
||||
controller.getOwnInstance()
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@
|
|||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.bookmarks</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.main-scene</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
|
|
|
@ -15,12 +15,15 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
|
||||
var window: UIWindow?
|
||||
|
||||
private var launchActivity: NSUserActivity?
|
||||
|
||||
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 }
|
||||
|
||||
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||
launchActivity = activity
|
||||
}
|
||||
|
||||
window = UIWindow(windowScene: windowScene)
|
||||
|
||||
if let report = AppDelegate.pendingCrashReport {
|
||||
|
@ -28,6 +31,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
handlePendingCrashReport(report, session: session)
|
||||
} else {
|
||||
showAppOrOnboardingUI(session: session)
|
||||
if connectionOptions.urlContexts.count > 0 {
|
||||
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
||||
}
|
||||
}
|
||||
|
||||
window!.makeKeyAndVisible()
|
||||
|
@ -50,20 +56,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
if url.host == "x-callback-url" {
|
||||
_ = XCBManager.handle(url: url)
|
||||
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let tabBarController = window!.rootViewController as? MainTabBarViewController,
|
||||
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
|
||||
|
||||
let rootViewController = window!.rootViewController as? TuskerRootViewController {
|
||||
components.scheme = "https"
|
||||
let query = url.absoluteString
|
||||
exploreController.searchController.searchBar.text = query
|
||||
exploreController.resultsController.performSearch(query: query)
|
||||
let query = components.string!
|
||||
rootViewController.performSearch(query: query)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,6 +94,14 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
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) {
|
||||
// Called as the scene transitions from the background to the foreground.
|
||||
// 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) {
|
||||
let session = session ?? window!.windowScene!.session
|
||||
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 {
|
||||
session.mastodonController = MastodonController.getForAccount(account)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
||||
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
||||
|
@ -52,10 +52,10 @@ final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
|||
|
||||
extension CompositionAttachment: Identifiable {}
|
||||
|
||||
private let imageType = kUTTypeImage as String
|
||||
private let mp4Type = kUTTypeMPEG4 as String
|
||||
private let quickTimeType = kUTTypeQuickTimeMovie as String
|
||||
private let dataType = kUTTypeData as String
|
||||
private let imageType = UTType.image.identifier
|
||||
private let mp4Type = UTType.mpeg4Movie.identifier
|
||||
private let quickTimeType = UTType.quickTimeMovie.identifier
|
||||
private let dataType = UTType.data.identifier
|
||||
|
||||
extension CompositionAttachment: NSItemProviderWriting {
|
||||
static var writableTypeIdentifiersForItemProvider: [String] {
|
||||
|
@ -100,11 +100,11 @@ extension CompositionAttachment: NSItemProviderReading {
|
|||
return try PropertyListDecoder().decode(Self.self, from: data)
|
||||
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
|
||||
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 temporaryFileName = ProcessInfo().globallyUniqueString
|
||||
let fileExt = UTTypeCopyPreferredTagWithClass(typeIdentifier as CFString, kUTTagClassFilenameExtension)!
|
||||
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt.takeUnretainedValue() as String)
|
||||
let fileExt = type.preferredFilenameExtension!
|
||||
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt)
|
||||
try data.write(to: temporaryFileURL)
|
||||
return CompositionAttachment(data: .video(temporaryFileURL)) as! Self
|
||||
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import Photos
|
||||
import MobileCoreServices
|
||||
import UniformTypeIdentifiers
|
||||
import PencilKit
|
||||
|
||||
enum CompositionAttachmentData {
|
||||
|
@ -74,7 +74,7 @@ enum CompositionAttachmentData {
|
|||
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
||||
mimeType = "image/jpeg"
|
||||
} else {
|
||||
mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String
|
||||
mimeType = UTType(dataUTI)!.preferredMIMEType!
|
||||
}
|
||||
|
||||
completion(data, mimeType)
|
||||
|
|
|
@ -63,6 +63,7 @@ class Preferences: Codable, ObservableObject {
|
|||
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||
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.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(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
||||
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
||||
|
||||
try container.encode(silentActions, forKey: .silentActions)
|
||||
try container.encode(statusContentType, forKey: .statusContentType)
|
||||
|
@ -133,6 +135,7 @@ class Preferences: Codable, ObservableObject {
|
|||
@Published var showFavoriteAndReblogCounts = true
|
||||
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
||||
@Published var grayscaleImages = false
|
||||
@Published var disableInfiniteScrolling = false
|
||||
|
||||
// MARK: Advanced
|
||||
@Published var silentActions: [String: Permission] = [:]
|
||||
|
@ -165,6 +168,7 @@ class Preferences: Codable, ObservableObject {
|
|||
case showFavoriteAndReblogCounts
|
||||
case defaultNotificationsType
|
||||
case grayscaleImages
|
||||
case disableInfiniteScrolling
|
||||
|
||||
case silentActions
|
||||
case statusContentType
|
||||
|
|
|
@ -12,7 +12,7 @@ import Photos
|
|||
private let reuseIdentifier = "assetCell"
|
||||
private let cameraReuseIdentifier = "showCameraCell"
|
||||
|
||||
protocol AssetCollectionViewControllerDelegate: class {
|
||||
protocol AssetCollectionViewControllerDelegate: AnyObject {
|
||||
func shouldSelectAsset(_ asset: PHAsset) -> Bool
|
||||
func didSelectAssets(_ assets: [PHAsset])
|
||||
func captureFromCamera()
|
||||
|
@ -70,6 +70,7 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
|
||||
collectionView.alwaysBounceVertical = true
|
||||
collectionView.allowsMultipleSelection = true
|
||||
collectionView.allowsSelection = true
|
||||
|
||||
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
||||
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
||||
|
@ -98,8 +99,6 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
}
|
||||
})
|
||||
|
||||
setEditing(true, animated: false)
|
||||
|
||||
updateItemsSelectedCount()
|
||||
|
||||
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Photos
|
||||
|
||||
protocol AssetPickerViewControllerDelegate: class {
|
||||
protocol AssetPickerViewControllerDelegate: AnyObject {
|
||||
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool
|
||||
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData])
|
||||
}
|
||||
|
|
|
@ -51,8 +51,14 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
}
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||
didSet {
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
return !isInteractivelyAnimatingDismissal
|
||||
}
|
||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||
return .none
|
||||
|
|
|
@ -41,6 +41,16 @@ struct ComposeAttachmentRow: View {
|
|||
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 {
|
||||
|
@ -126,7 +136,9 @@ struct ComposeAttachmentRow: View {
|
|||
}
|
||||
|
||||
private func removeAttachment() {
|
||||
draft.attachments.removeAll { $0.id == attachment.id }
|
||||
withAnimation {
|
||||
draft.attachments.removeAll { $0.id == attachment.id }
|
||||
}
|
||||
}
|
||||
|
||||
private func editDrawing() {
|
||||
|
|
|
@ -69,6 +69,7 @@ struct ComposeAttachmentsList: View {
|
|||
.frame(height: cellHeight / 2)
|
||||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
.frame(height: totalListHeight)
|
||||
.onAppear(perform: self.didAppear)
|
||||
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import PencilKit
|
||||
|
||||
protocol ComposeDrawingViewControllerDelegate: class {
|
||||
protocol ComposeDrawingViewControllerDelegate: AnyObject {
|
||||
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
|
||||
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ class ComposeDrawingViewController: UIViewController {
|
|||
private(set) var undoBarButtonItem: UIBarButtonItem!
|
||||
private(set) var redoBarButtonItem: UIBarButtonItem!
|
||||
|
||||
private var toolPicker: PKToolPicker!
|
||||
|
||||
private var initialDrawing: PKDrawing?
|
||||
|
||||
init() {
|
||||
|
@ -70,26 +72,21 @@ class ComposeDrawingViewController: UIViewController {
|
|||
canvasView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
|
||||
toolPicker = PKToolPicker()
|
||||
toolPicker.setVisible(true, forFirstResponder: canvasView)
|
||||
toolPicker.addObserver(canvasView)
|
||||
toolPicker.addObserver(self)
|
||||
|
||||
updateLayout(for: toolPicker)
|
||||
canvasView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(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.addObserver(canvasView)
|
||||
toolPicker.addObserver(self)
|
||||
|
||||
updateLayout(for: toolPicker)
|
||||
canvasView.becomeFirstResponder()
|
||||
|
||||
// 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: .NSUndoManagerDidRedoChange, 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!)
|
||||
}
|
||||
|
||||
func updateLayout(for toolPicker: PKToolPicker) {
|
||||
|
|
|
@ -11,7 +11,7 @@ import Combine
|
|||
import Pachyderm
|
||||
import PencilKit
|
||||
|
||||
protocol ComposeHostingControllerDelegate: class {
|
||||
protocol ComposeHostingControllerDelegate: AnyObject {
|
||||
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
||||
}
|
||||
|
||||
|
@ -245,6 +245,25 @@ extension ComposeHostingController: ComposeUIStateDelegate {
|
|||
}
|
||||
|
||||
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()
|
||||
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||
self.present(sheetContainer, animated: true)
|
||||
|
@ -288,7 +307,9 @@ extension ComposeHostingController: AssetPickerViewControllerDelegate {
|
|||
let attachments = attachments.map {
|
||||
CompositionAttachment(data: $0)
|
||||
}
|
||||
draft.attachments.append(contentsOf: attachments)
|
||||
withAnimation {
|
||||
draft.attachments.append(contentsOf: attachments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ struct ComposePollView: View {
|
|||
}
|
||||
.accentColor(buttonForegroundColor)
|
||||
.background(Circle().foregroundColor(buttonBackgroundColor))
|
||||
.hoverEffect()
|
||||
}
|
||||
|
||||
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
|
||||
|
@ -161,6 +162,7 @@ struct ComposePollOption: View {
|
|||
}
|
||||
.foregroundColor(poll.options.count == 1 ? .gray : .red)
|
||||
.disabled(poll.options.count == 1)
|
||||
.hoverEffect()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,9 +52,20 @@ struct ComposeReplyView: View {
|
|||
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
||||
// 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
|
||||
var scrollOffset = outerMinY - geometry.frame(in: .global).minY
|
||||
scrollOffset += stackPadding
|
||||
let offset = min(max(scrollOffset, 0), geometry.size.height - 50 - stackPadding)
|
||||
let scrollOffset = outerMinY - geometry.frame(in: .global).minY
|
||||
|
||||
// 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)
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
protocol ComposeTextViewCaretScrolling: class {
|
||||
protocol ComposeTextViewCaretScrolling: AnyObject {
|
||||
var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set }
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
protocol ComposeUIStateDelegate: class {
|
||||
protocol ComposeUIStateDelegate: AnyObject {
|
||||
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
|
||||
|
||||
func dismissCompose(mode: ComposeUIState.DismissMode)
|
||||
|
@ -60,6 +60,6 @@ extension ComposeUIState {
|
|||
}
|
||||
}
|
||||
|
||||
protocol ComposeAutocompleteHandler: class {
|
||||
protocol ComposeAutocompleteHandler: AnyObject {
|
||||
func autocomplete(with string: String)
|
||||
}
|
||||
|
|
|
@ -58,8 +58,10 @@ struct ComposeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
||||
if postProgress > 0 {
|
||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
||||
}
|
||||
|
||||
autocompleteSuggestions
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
|||
|
||||
private let reuseIdentifier = "EmojiCell"
|
||||
|
||||
protocol EmojiPickerCollectionViewControllerDelegate: class {
|
||||
protocol EmojiPickerCollectionViewControllerDelegate: AnyObject {
|
||||
func selectedEmoji(_ emoji: Emoji)
|
||||
}
|
||||
|
||||
|
|
|
@ -132,6 +132,30 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||
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)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
|
@ -139,12 +163,10 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||
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
|
||||
mainStatus.incrementReferenceCount()
|
||||
|
||||
// todo: it would be nice to cache these contexts
|
||||
let request = Status.getContext(mainStatusID)
|
||||
mastodonController.run(request) { response in
|
||||
guard case let .success(context, _) = response else { fatalError() }
|
||||
|
@ -304,7 +326,16 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
|
||||
@objc func toggleVisibilityButtonPressed() {
|
||||
#if SDK_IOS_15
|
||||
if #available(iOS 15.0, *) {
|
||||
visibilityBarButtonItem.isSelected = !visibilityBarButtonItem.isSelected
|
||||
showStatusesAutomatically = visibilityBarButtonItem.isSelected
|
||||
} else {
|
||||
showStatusesAutomatically = !showStatusesAutomatically
|
||||
}
|
||||
#else
|
||||
showStatusesAutomatically = !showStatusesAutomatically
|
||||
#endif
|
||||
|
||||
let snapshot = dataSource.snapshot()
|
||||
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
protocol DraftsTableViewControllerDelegate: class {
|
||||
protocol DraftsTableViewControllerDelegate: AnyObject {
|
||||
func draftSelectionCanceled()
|
||||
func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void)
|
||||
func draftSelected(_ draft: Draft)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
protocol FastAccountSwitcherViewControllerDelegate: class {
|
||||
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
||||
}
|
||||
|
||||
|
|
|
@ -51,8 +51,14 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
private var prevZoomScale: CGFloat?
|
||||
private var isGrayscale = false
|
||||
|
||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||
didSet {
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
return !isInteractivelyAnimatingDismissal
|
||||
}
|
||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||
return .none
|
||||
|
|
|
@ -43,8 +43,14 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
var animationGifData: Data? { largeImageVC?.animationGifData }
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||
didSet {
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
return !isInteractivelyAnimatingDismissal
|
||||
}
|
||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||
return .none
|
||||
|
|
|
@ -15,6 +15,7 @@ protocol LargeImageAnimatableViewController: UIViewController {
|
|||
var animationImage: UIImage? { get }
|
||||
var animationGifData: Data? { get }
|
||||
var dismissInteractionController: LargeImageInteractionController? { get }
|
||||
var isInteractivelyAnimatingDismissal: Bool { get set }
|
||||
}
|
||||
|
||||
extension LargeImageAnimatableViewController {
|
||||
|
@ -74,7 +75,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
|||
toVC.largeImageController?.contentView.isHidden = true
|
||||
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 newHeight = finalFrameSize.height / image.size.height
|
||||
if newHeight < newWidth {
|
||||
|
|
|
@ -14,9 +14,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
|||
var direction: CGFloat?
|
||||
|
||||
var shouldCompleteTransition = false
|
||||
private weak var viewController: UIViewController!
|
||||
private weak var viewController: LargeImageAnimatableViewController!
|
||||
|
||||
init(viewController: UIViewController) {
|
||||
init(viewController: LargeImageAnimatableViewController) {
|
||||
super.init()
|
||||
self.viewController = viewController
|
||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
||||
|
@ -42,6 +42,7 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
|||
viewController.dismiss(animated: true)
|
||||
case .changed:
|
||||
shouldCompleteTransition = progress > 0.5 || velocity > 1000
|
||||
viewController.isInteractivelyAnimatingDismissal = progress > 0.1
|
||||
update(progress)
|
||||
case .cancelled:
|
||||
inProgress = false
|
||||
|
@ -59,4 +60,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
|||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
super.cancel()
|
||||
viewController.isInteractivelyAnimatingDismissal = false
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ class ListTimelineViewController: TimelineTableViewController {
|
|||
dismiss(animated: true)
|
||||
|
||||
// todo: show loading indicator
|
||||
reloadInitialItems()
|
||||
reloadInitial()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -71,14 +71,21 @@ extension AccountSwitchingContainerViewController {
|
|||
|
||||
extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||
func presentCompose() {
|
||||
loadViewIfNeeded()
|
||||
root.presentCompose()
|
||||
}
|
||||
|
||||
func select(tab: MainTabBarViewController.Tab) {
|
||||
loadViewIfNeeded()
|
||||
root.select(tab: tab)
|
||||
}
|
||||
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||
root.getTabController(tab: tab)
|
||||
loadViewIfNeeded()
|
||||
return root.getTabController(tab: tab)
|
||||
}
|
||||
|
||||
func performSearch(query: String) {
|
||||
root.performSearch(query: query)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
protocol MainSidebarViewControllerDelegate: class {
|
||||
protocol MainSidebarViewControllerDelegate: AnyObject {
|
||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ class MainSidebarViewController: UIViewController {
|
|||
|
||||
private(set) var previouslySelectedItem: Item?
|
||||
var selectedItem: Item? {
|
||||
guard let indexPath = collectionView.indexPathsForSelectedItems?.first else {
|
||||
guard let indexPath = collectionView?.indexPathsForSelectedItems?.first else {
|
||||
return nil
|
||||
}
|
||||
return dataSource.itemIdentifier(for: indexPath)
|
||||
|
@ -448,6 +448,20 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
|||
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 {
|
||||
|
|
|
@ -19,6 +19,10 @@ class MainSplitViewController: UISplitViewController {
|
|||
|
||||
private var tabBarViewController: MainTabBarViewController!
|
||||
|
||||
private var secondaryNavController: UINavigationController! {
|
||||
viewController(for: .secondary) as? UINavigationController
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
return .portrait
|
||||
|
@ -67,8 +71,7 @@ class MainSplitViewController: UISplitViewController {
|
|||
}
|
||||
|
||||
func select(item: MainSidebarViewController.Item) {
|
||||
let nav = viewController(for: .secondary) as! UINavigationController
|
||||
nav.viewControllers = getOrCreateNavigationStack(item: item)
|
||||
secondaryNavController.viewControllers = getOrCreateNavigationStack(item: item)
|
||||
}
|
||||
|
||||
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) {
|
||||
var itemNavStack: [UIViewController]
|
||||
if item == sidebar.selectedItem {
|
||||
let detailNav = viewController(for: .secondary) as! UINavigationController
|
||||
itemNavStack = detailNav.viewControllers
|
||||
itemNavStack = secondaryNavController.viewControllers
|
||||
} else {
|
||||
itemNavStack = navigationStacks[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
|
||||
explore.loadViewIfNeeded()
|
||||
|
||||
let nav = viewController(for: .secondary) as! UINavigationController
|
||||
let search = nav.viewControllers.first as! SearchViewController
|
||||
let search = secondaryNavController.viewControllers.first as! SearchViewController
|
||||
// Copy the search query from the search VC to the Explore VC's search controller.
|
||||
let query = search.searchController.searchBar.text ?? ""
|
||||
explore.searchController.searchBar.text = query
|
||||
|
@ -318,9 +319,8 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
|||
}
|
||||
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) {
|
||||
let nav = viewController(for: .secondary) as! UINavigationController
|
||||
if let previous = sidebar.previouslySelectedItem {
|
||||
navigationStacks[previous] = nav.viewControllers
|
||||
navigationStacks[previous] = secondaryNavController.viewControllers
|
||||
}
|
||||
select(item: item)
|
||||
}
|
||||
|
@ -353,10 +353,17 @@ fileprivate extension MainSidebarViewController.Item {
|
|||
|
||||
extension MainSplitViewController: TuskerRootViewController {
|
||||
@objc func presentCompose() {
|
||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||
nav.presentationController?.delegate = vc
|
||||
present(nav, animated: true)
|
||||
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 nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||
nav.presentationController?.delegate = vc
|
||||
present(nav, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func select(tab: MainTabBarViewController.Tab) {
|
||||
|
@ -366,8 +373,15 @@ extension MainSplitViewController: TuskerRootViewController {
|
|||
if tab == .compose {
|
||||
presentCompose()
|
||||
} else {
|
||||
select(item: .tab(tab))
|
||||
sidebar.select(item: .tab(tab), animated: false)
|
||||
if presentedViewController != nil {
|
||||
dismiss(animated: true) {
|
||||
self.select(item: .tab(tab))
|
||||
self.sidebar.select(item: .tab(tab), animated: false)
|
||||
}
|
||||
} else {
|
||||
select(item: .tab(tab))
|
||||
sidebar.select(item: .tab(tab), animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -379,12 +393,43 @@ extension MainSplitViewController: TuskerRootViewController {
|
|||
if tab == .compose {
|
||||
return nil
|
||||
} else if case .tab(tab) = sidebar.selectedItem {
|
||||
return viewController(for: .secondary)
|
||||
return secondaryNavController
|
||||
} else {
|
||||
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 {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||
|
||||
|
@ -16,6 +15,9 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
private var composePlaceholder: UIViewController!
|
||||
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||
|
||||
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
||||
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
var selectedTab: Tab {
|
||||
return Tab(rawValue: selectedIndex)!
|
||||
}
|
||||
|
@ -71,9 +73,62 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
tapRecognizer.cancelsTouchesInView = false
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
fastAccountSwitcher.hide()
|
||||
}
|
||||
|
@ -138,6 +193,8 @@ extension MainTabBarViewController {
|
|||
if tab == .compose {
|
||||
return nil
|
||||
} else {
|
||||
// viewWControllers array is setup in viewDidLoad
|
||||
loadViewIfNeeded()
|
||||
return viewControllers![tab.rawValue]
|
||||
}
|
||||
}
|
||||
|
@ -145,13 +202,9 @@ extension MainTabBarViewController {
|
|||
|
||||
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||
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 {
|
||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||
return false
|
||||
}
|
||||
|
||||
let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view)
|
||||
return myProfileButton.bounds.contains(locationInButton)
|
||||
}
|
||||
|
@ -159,19 +212,56 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
|||
|
||||
extension MainTabBarViewController: TuskerRootViewController {
|
||||
@objc func presentCompose() {
|
||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||
nav.presentationController?.delegate = vc
|
||||
present(nav, animated: true)
|
||||
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 nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||
nav.presentationController?.delegate = vc
|
||||
present(nav, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func select(tab: Tab) {
|
||||
if tab == .compose {
|
||||
presentCompose()
|
||||
} else {
|
||||
selectedIndex = tab.rawValue
|
||||
// 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 {
|
||||
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 {
|
||||
|
|
|
@ -12,4 +12,5 @@ protocol TuskerRootViewController: UIViewController {
|
|||
func presentCompose()
|
||||
func select(tab: MainTabBarViewController.Tab)
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
||||
func performSearch(query: String)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import Pachyderm
|
||||
|
||||
protocol InstanceSelectorTableViewControllerDelegate: class {
|
||||
protocol InstanceSelectorTableViewControllerDelegate: AnyObject {
|
||||
func didSelectInstance(url: URL)
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@ struct PreferencesView: View {
|
|||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}.onDrag {
|
||||
let activity = UserActivityManager.mainSceneActivity(accountID: account.id)
|
||||
return NSItemProvider(object: activity)
|
||||
}
|
||||
}.onDelete { (indices: IndexSet) in
|
||||
var indices = indices
|
||||
|
|
|
@ -16,6 +16,7 @@ struct WellnessPrefsView: View {
|
|||
showFavAndReblogCount
|
||||
notificationsMode
|
||||
grayscaleImages
|
||||
disableInfiniteScrolling
|
||||
}
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.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 {
|
||||
|
|
|
@ -91,11 +91,18 @@ class ProfileViewController: UIPageViewController {
|
|||
addKeyCommand(MenuController.nextSubTabCommand)
|
||||
|
||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.sink { [weak self] (_) in self?.updateAccountUI() }
|
||||
|
||||
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) {
|
||||
|
@ -245,34 +252,6 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
|||
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 {
|
||||
|
|
|
@ -14,7 +14,7 @@ fileprivate let accountCell = "accountCell"
|
|||
fileprivate let statusCell = "statusCell"
|
||||
fileprivate let hashtagCell = "hashtagCell"
|
||||
|
||||
protocol SearchResultsViewControllerDelegate: class {
|
||||
protocol SearchResultsViewControllerDelegate: AnyObject {
|
||||
func selectedSearchResult(account accountID: String)
|
||||
func selectedSearchResult(hashtag: Hashtag)
|
||||
func selectedSearchResult(status statusID: String)
|
||||
|
@ -36,6 +36,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
|
||||
private var activityIndicator: UIActivityIndicatorView!
|
||||
private var errorLabel: UILabel!
|
||||
|
||||
/// Types of results to search for. `nil` means all results will be included.
|
||||
var resultTypes: [SearchResultType]? = nil
|
||||
|
@ -61,6 +62,22 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
override func 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: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
||||
tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell)
|
||||
|
@ -131,57 +148,77 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
|
||||
activityIndicator.isHidden = false
|
||||
activityIndicator.startAnimating()
|
||||
errorLabel.isHidden = true
|
||||
|
||||
let resultTypes = self.resultTypes
|
||||
let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10)
|
||||
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 {
|
||||
self.activityIndicator.isHidden = true
|
||||
self.activityIndicator.stopAnimating()
|
||||
}
|
||||
self.showSearchResults(results)
|
||||
case let .failure(error):
|
||||
DispatchQueue.main.async {
|
||||
self.activityIndicator.isHidden = true
|
||||
self.activityIndicator.stopAnimating()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.activityIndicator.isHidden = true
|
||||
self.activityIndicator.stopAnimating()
|
||||
self.showSearchError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showSearchResults(_ results: SearchResults) {
|
||||
let oldSnapshot = self.dataSource.snapshot()
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
|
||||
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
|
||||
if oldSnapshot.indexOfSection(.accounts) != nil {
|
||||
oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in
|
||||
guard case let .account(id) = item else { return }
|
||||
self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
guard self.currentQuery == query else { return }
|
||||
if oldSnapshot.indexOfSection(.statuses) != nil {
|
||||
oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in
|
||||
guard case let .status(id, _) = item else { return }
|
||||
self.mastodonController.persistentContainer.status(for: id, in: context)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
let oldSnapshot = self.dataSource.snapshot()
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
let resultTypes = self.resultTypes
|
||||
if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) {
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
||||
addAccounts(results.accounts)
|
||||
}
|
||||
if !results.hashtags.isEmpty && (resultTypes == nil || resultTypes!.contains(.hashtags)) {
|
||||
snapshot.appendSections([.hashtags])
|
||||
snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags)
|
||||
}
|
||||
if !results.statuses.isEmpty && (resultTypes == nil || resultTypes!.contains(.statuses)) {
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
||||
addStatuses(results.statuses)
|
||||
}
|
||||
}, completion: {
|
||||
DispatchQueue.main.async {
|
||||
self.errorLabel.isHidden = true
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
|
||||
if oldSnapshot.indexOfSection(.accounts) != nil {
|
||||
oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in
|
||||
guard case let .account(id) = item else { return }
|
||||
self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
private func showSearchError(_ error: Client.Error) {
|
||||
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
dataSource.apply(snapshot)
|
||||
|
||||
if oldSnapshot.indexOfSection(.statuses) != nil {
|
||||
oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in
|
||||
guard case let .status(id, _) = item else { return }
|
||||
self.mastodonController.persistentContainer.status(for: id, in: context)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) {
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
||||
addAccounts(results.accounts)
|
||||
}
|
||||
if !results.hashtags.isEmpty && (resultTypes == nil || resultTypes!.contains(.hashtags)) {
|
||||
snapshot.appendSections([.hashtags])
|
||||
snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags)
|
||||
}
|
||||
if !results.statuses.isEmpty && (resultTypes == nil || resultTypes!.contains(.statuses)) {
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
||||
addStatuses(results.statuses)
|
||||
}
|
||||
}, completion: {
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
})
|
||||
}
|
||||
errorLabel.isHidden = false
|
||||
errorLabel.text = error.localizedDescription
|
||||
}
|
||||
|
||||
// MARK: - Table view delegate
|
||||
|
|
|
@ -37,9 +37,10 @@ class SearchViewController: UIViewController {
|
|||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||
resultsController.exploreNavigationController = self.navigationController
|
||||
searchController = UISearchController(searchResultsController: resultsController)
|
||||
searchController.searchResultsUpdater = resultsController
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.searchBar.autocapitalizationType = .none
|
||||
searchController.searchBar.delegate = resultsController
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
definesPresentationContext = true
|
||||
|
||||
navigationItem.searchController = searchController
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
protocol InstanceTimelineViewControllerDelegate: class {
|
||||
protocol InstanceTimelineViewControllerDelegate: AnyObject {
|
||||
func didSaveInstance(url: URL)
|
||||
func didUnsaveInstance(url: URL)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
|||
|
||||
typealias TimelineEntry = (id: String, state: StatusState)
|
||||
|
||||
class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry> {
|
||||
class TimelineTableViewController: DiffableTimelineLikeTableViewController<TimelineTableViewController.Section, TimelineTableViewController.Item> {
|
||||
|
||||
let timeline: Timeline
|
||||
weak var mastodonController: MastodonController!
|
||||
|
@ -19,6 +19,8 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
|||
private var newer: RequestRange?
|
||||
private var older: RequestRange?
|
||||
|
||||
private var didConfirmLoadMore = false
|
||||
|
||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||
self.timeline = timeline
|
||||
self.mastodonController = mastodonController
|
||||
|
@ -40,14 +42,14 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
|||
}
|
||||
|
||||
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
|
||||
// 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(_:)
|
||||
for section in sections {
|
||||
for (id, _) in section {
|
||||
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
// todo: remove the whole reference count system
|
||||
for case let .status(id: id, state: _) in dataSource.snapshot().itemIdentifiers {
|
||||
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,100 +57,157 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
|||
super.viewDidLoad()
|
||||
|
||||
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 {
|
||||
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
|
||||
}
|
||||
|
||||
override func willRemoveRows(at indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
let id = item(for: indexPath).id
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
override func timelineContentSections() -> [Section] {
|
||||
return [.statuses]
|
||||
}
|
||||
|
||||
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) {
|
||||
guard let mastodonController = mastodonController else {
|
||||
completion(.failure(.noClient))
|
||||
return
|
||||
}
|
||||
|
||||
let request = Client.getStatuses(timeline: timeline)
|
||||
|
||||
mastodonController?.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
mastodonController.run(request) { response in
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
completion(.failure(.client(error)))
|
||||
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
case let .success(statuses, pagination):
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
||||
completion(statuses.map { ($0.id, .unknown) })
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
mastodonController.run(request) { response in
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
completion(.failure(.client(error)))
|
||||
|
||||
self.older = pagination?.older
|
||||
case let .success(statuses, pagination):
|
||||
self.older = pagination?.older
|
||||
|
||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
||||
completion(statuses.map { ($0.id, .unknown) })
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
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 {
|
||||
completion([])
|
||||
completion(.failure(.noNewer))
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
guard case let .success(statuses, pagination) = response else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
case let .success(statuses, pagination):
|
||||
// if there are no new statuses, pagination is nil
|
||||
// if we were to then overwrite self.newer, future refresh would fail
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
// if there are no new statuses, pagination is nil
|
||||
// if we were to then overwrite self.newer, future refreshes would fail
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
||||
completion(statuses.map { ($0.id, .unknown) })
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
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 tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
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
|
||||
override func willRemoveItems(_ items: [Item]) {
|
||||
for case let .status(id: id, state: _) in items {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TimelineTableViewController {
|
||||
enum Section: Hashable, CaseIterable {
|
||||
case statuses
|
||||
case footer
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case status(id: String, state: StatusState)
|
||||
case confirmLoadMore
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineTableViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
}
|
||||
|
@ -161,17 +220,23 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
|
|||
|
||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||
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)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
let ids: [String] = indexPaths.compactMap {
|
||||
guard $0.section < sections.count,
|
||||
$0.row < sections[$0.section].count else {
|
||||
if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
|
||||
return id
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return item(for: $0).id
|
||||
}
|
||||
cancelPrefetchingStatuses(with: ids)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
import SafariServices
|
||||
import Pachyderm
|
||||
|
||||
protocol MenuPreviewProvider: class {
|
||||
protocol MenuPreviewProvider: AnyObject {
|
||||
|
||||
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
|
||||
|
||||
|
@ -37,10 +37,11 @@ extension MenuPreviewProvider {
|
|||
guard let mastodonController = mastodonController,
|
||||
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 [
|
||||
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 }
|
||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||
})
|
||||
|
@ -63,38 +64,43 @@ extension MenuPreviewProvider {
|
|||
let request = Client.getRelationships(accounts: [account.id])
|
||||
// talk about callback hell :/
|
||||
mastodonController.run(request) { [weak self] (response) in
|
||||
if let self = self,
|
||||
case let .success(results, _) = response,
|
||||
let relationship = results.first {
|
||||
let following = relationship.following
|
||||
DispatchQueue.main.async {
|
||||
elementHandler([
|
||||
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)
|
||||
mastodonController.run(request) { (response) in
|
||||
switch response {
|
||||
case .failure(_):
|
||||
fatalError()
|
||||
case let .success(relationship, _):
|
||||
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||
}
|
||||
}
|
||||
})
|
||||
])
|
||||
}
|
||||
guard let self = self,
|
||||
case let .success(results, _) = response,
|
||||
let relationship = results.first else {
|
||||
elementHandler([])
|
||||
return
|
||||
}
|
||||
let following = relationship.following
|
||||
DispatchQueue.main.async {
|
||||
let action = 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)
|
||||
mastodonController.run(request) { (response) in
|
||||
switch response {
|
||||
case .failure(_):
|
||||
fatalError()
|
||||
case let .success(relationship, _):
|
||||
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||
}
|
||||
}
|
||||
})
|
||||
elementHandler([
|
||||
action
|
||||
])
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
let shareSection = [
|
||||
var shareSection = [
|
||||
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 }
|
||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||
})
|
||||
]
|
||||
|
||||
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID))
|
||||
|
||||
return [
|
||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
||||
|
@ -104,7 +110,7 @@ extension MenuPreviewProvider {
|
|||
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
||||
return [
|
||||
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 }
|
||||
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 mastodonController.loggedIn else {
|
||||
guard let accountID = mastodonController.accountInfo?.id else {
|
||||
// user is logged out
|
||||
return [
|
||||
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 }
|
||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||
})
|
||||
|
@ -150,10 +157,6 @@ extension MenuPreviewProvider {
|
|||
let muted = status.muted
|
||||
|
||||
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
|
||||
guard let self = self else { return }
|
||||
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 {
|
||||
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 }
|
||||
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||
|
@ -204,21 +214,13 @@ extension MenuPreviewProvider {
|
|||
|
||||
var shareSection = [
|
||||
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 }
|
||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||
}),
|
||||
]
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
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
|
||||
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
||||
|
||||
return [
|
||||
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 {
|
||||
|
|
|
@ -44,6 +44,13 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
|||
|
||||
addKeyCommand(MenuController.prevSubTabCommand)
|
||||
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) {
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
import UIKit
|
||||
|
||||
/// 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,
|
||||
// refreshing, and pruning offscreen rows automatically.
|
||||
/// For example, this class handles loading new items when the user scrolls to the end,
|
||||
/// refreshing, and pruning offscreen rows automatically.
|
||||
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
|
||||
|
||||
private(set) var loaded = false
|
||||
|
|
|
@ -38,6 +38,15 @@ class UserActivityManager {
|
|||
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
|
||||
static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity {
|
||||
// todo: update to use managed objects
|
||||
|
@ -61,6 +70,7 @@ class UserActivityManager {
|
|||
// TODO: check not currently showing compose screen
|
||||
let mentioning = activity.userInfo?["mentioning"] as? String
|
||||
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)
|
||||
present(UINavigationController(rootViewController: composeVC))
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import Foundation
|
||||
|
||||
enum UserActivityType: String {
|
||||
case mainScene = "space.vaccor.Tusker.activity.main-scene"
|
||||
case newPost = "space.vaccor.Tusker.activity.new-post"
|
||||
case checkNotifications = "space.vaccor.Tusker.activity.check-notifications"
|
||||
case showTimeline = "space.vaccor.Tusker.activity.show-timeline"
|
||||
|
@ -22,6 +23,8 @@ enum UserActivityType: String {
|
|||
extension UserActivityType {
|
||||
var handle: (NSUserActivity) -> Void {
|
||||
switch self {
|
||||
case .mainScene:
|
||||
fatalError("cannot handle main scene activity")
|
||||
case .newPost:
|
||||
return UserActivityManager.handleNewPost
|
||||
case .checkNotifications:
|
||||
|
|
|
@ -89,15 +89,22 @@ extension TuskerNavigationDelegate {
|
|||
}
|
||||
|
||||
func compose(editing draft: Draft) {
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||
|
||||
let vc = UINavigationController(rootViewController: compose)
|
||||
vc.presentationController?.delegate = compose
|
||||
present(vc, animated: true)
|
||||
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 nav = UINavigationController(rootViewController: compose)
|
||||
nav.presentationController?.delegate = compose
|
||||
present(nav, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
||||
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
||||
DraftsManager.shared.add(draft)
|
||||
compose(editing: draft)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
|||
import Gifu
|
||||
import AVFoundation
|
||||
|
||||
protocol AttachmentViewDelegate: class {
|
||||
protocol AttachmentViewDelegate: AnyObject {
|
||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
|
||||
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)
|
||||
|
||||
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = [.image, .button]
|
||||
}
|
||||
|
||||
@objc private func preferencesChanged() {
|
||||
|
@ -161,7 +164,9 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
|||
let attachmentURL = attachment.url
|
||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
|
||||
guard let self = self, let data = data else { return }
|
||||
self.attachmentRequest = nil
|
||||
DispatchQueue.main.async {
|
||||
self.attachmentRequest = nil
|
||||
}
|
||||
if self.attachment.url.pathExtension == "gif" {
|
||||
self.source = .gifData(attachmentURL, data)
|
||||
if self.autoplayGifs {
|
||||
|
@ -294,6 +299,13 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
|||
showGallery()
|
||||
}
|
||||
|
||||
// MARK: - Accessibility
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
showGallery()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate extension AttachmentView {
|
||||
|
|
|
@ -248,9 +248,11 @@ class AttachmentsContainerView: UIView {
|
|||
let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size)
|
||||
attachmentView.delegate = delegate
|
||||
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 = "Attachment \(index + 1)"
|
||||
if let desc = attachments[index].description {
|
||||
attachmentView.accessibilityLabel! += ", \(desc)"
|
||||
}
|
||||
attachmentViews.add(attachmentView)
|
||||
return attachmentView
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
|||
|
||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||
|
||||
protocol BaseEmojiLabel: class {
|
||||
protocol BaseEmojiLabel: AnyObject {
|
||||
var emojiIdentifier: String? { get set }
|
||||
var emojiRequests: [ImageCache.Request] { get set }
|
||||
var emojiFont: UIFont { get }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -30,6 +30,7 @@ class PollOptionView: UIView {
|
|||
let label = EmojiLabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.numberOfLines = 0
|
||||
label.font = .preferredFont(forTextStyle: .callout)
|
||||
label.text = option.title
|
||||
label.setEmojis(poll.emojis, identifier: poll.id)
|
||||
addSubview(label)
|
||||
|
@ -37,15 +38,21 @@ class PollOptionView: UIView {
|
|||
let percentLabel = UILabel()
|
||||
percentLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
percentLabel.text = "100%"
|
||||
percentLabel.font = label.font
|
||||
percentLabel.isHidden = true
|
||||
percentLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
percentLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
addSubview(percentLabel)
|
||||
|
||||
accessibilityLabel = option.title
|
||||
|
||||
if (poll.voted ?? false) || poll.effectiveExpired,
|
||||
let optionVotes = option.votesCount {
|
||||
let frac = poll.votesCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(poll.votesCount)
|
||||
let percent = String(format: "%.0f%%", frac * 100)
|
||||
|
||||
percentLabel.isHidden = false
|
||||
percentLabel.text = String(format: "%.0f%%", frac * 100)
|
||||
percentLabel.text = percent
|
||||
|
||||
let fillView = UIView()
|
||||
fillView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -60,6 +67,8 @@ class PollOptionView: UIView {
|
|||
fillView.topAnchor.constraint(equalTo: topAnchor),
|
||||
fillView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
accessibilityLabel! += ", \(percent)"
|
||||
}
|
||||
|
||||
let minHeightConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
|
||||
|
@ -76,12 +85,14 @@ class PollOptionView: UIView {
|
|||
label.topAnchor.constraint(equalTo: topAnchor),
|
||||
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
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.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||
])
|
||||
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
|
@ -69,6 +69,8 @@ class PollOptionsView: UIControl {
|
|||
stack.addArrangedSubview(optionView)
|
||||
return optionView
|
||||
}
|
||||
|
||||
accessibilityElements = options
|
||||
}
|
||||
|
||||
private func selectOption(_ option: PollOptionView) {
|
||||
|
|
|
@ -23,17 +23,20 @@ class StatusPollView: UIView {
|
|||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var statusID: String!
|
||||
private var poll: Poll!
|
||||
private(set) var poll: Poll?
|
||||
|
||||
private var optionsView: PollOptionsView!
|
||||
private var voteButton: UIButton!
|
||||
private var infoLabel: UILabel!
|
||||
private var options: [PollOptionView] = []
|
||||
|
||||
private var canVote = true
|
||||
private var animator: UIViewPropertyAnimator!
|
||||
private var currentSelectedOptionIndex: Int!
|
||||
|
||||
var isTracking: Bool {
|
||||
optionsView.isTracking
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
|
@ -74,13 +77,15 @@ class StatusPollView: UIView {
|
|||
voteButton.topAnchor.constraint(equalTo: optionsView.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.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
|
||||
if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account.id {
|
||||
|
@ -138,7 +143,7 @@ class StatusPollView: UIView {
|
|||
|
||||
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
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
|
|
|
@ -12,8 +12,6 @@ import Combine
|
|||
|
||||
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate {
|
||||
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int)
|
||||
|
||||
func profileHeader(_ headerView: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView)
|
||||
}
|
||||
|
||||
class ProfileHeaderView: UIView {
|
||||
|
@ -82,14 +80,14 @@ class ProfileHeaderView: UIView {
|
|||
cancellables = []
|
||||
|
||||
mastodonController.persistentContainer.accountSubject
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.sink { [weak self] in self?.updateUI(for: $0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
mastodonController.persistentContainer.relationshipSubject
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.sink { [weak self] (_) in self?.updateRelationship() }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
@ -134,6 +132,7 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
var fieldAccessibilityElements = [Any]()
|
||||
for field in account.fields {
|
||||
let nameLabel = EmojiLabel()
|
||||
nameLabel.text = field.name
|
||||
|
@ -157,7 +156,18 @@ class ProfileHeaderView: UIView {
|
|||
fieldValuesStackView.addArrangedSubview(valueTextView)
|
||||
|
||||
nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true
|
||||
fieldAccessibilityElements.append(nameLabel)
|
||||
fieldAccessibilityElements.append(valueTextView)
|
||||
}
|
||||
|
||||
accessibilityElements = [
|
||||
displayNameLabel!,
|
||||
usernameLabel!,
|
||||
noteTextView!,
|
||||
] + fieldAccessibilityElements + [
|
||||
moreButton!,
|
||||
pagesSegmentedControl!,
|
||||
]
|
||||
}
|
||||
|
||||
private func updateRelationship() {
|
||||
|
@ -193,11 +203,14 @@ class ProfileHeaderView: UIView {
|
|||
let image = image,
|
||||
self.accountID == accountID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||
DispatchQueue.main.async {
|
||||
self?.avatarRequest = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
self.avatarRequest = nil
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarRequest = nil
|
||||
self.avatarImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
|
@ -207,11 +220,14 @@ class ProfileHeaderView: UIView {
|
|||
let image = image,
|
||||
self.accountID == accountID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
|
||||
DispatchQueue.main.async {
|
||||
self?.headerRequest = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
self.headerRequest = nil
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.headerRequest = nil
|
||||
self.headerImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,8 +94,7 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
collapseButton.layer.masksToBounds = true
|
||||
collapseButton.layer.cornerRadius = 5
|
||||
|
||||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
||||
attachmentsView.isAccessibilityElement = true
|
||||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!, pollView!]
|
||||
|
||||
moreButton.showsMenuAsPrimaryAction = true
|
||||
|
||||
|
@ -107,8 +106,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
open func createObserversIfNecessary() {
|
||||
if statusUpdater == nil {
|
||||
statusUpdater = mastodonController.persistentContainer.statusSubject
|
||||
.filter { [unowned self] in $0 == self.statusID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.filter { [unowned self] in $0 == self.statusID }
|
||||
.sink { [unowned self] in
|
||||
if let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: $0) {
|
||||
|
@ -119,8 +118,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
|
||||
if accountUpdater == nil {
|
||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||
.filter { [unowned self] in $0 == self.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.filter { [unowned self] in $0 == self.accountID }
|
||||
.sink { [unowned self] in
|
||||
if let mastodonController = mastodonController,
|
||||
let account = mastodonController.persistentContainer.account(for: $0) {
|
||||
|
@ -157,8 +156,6 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
cardView.navigationDelegate = navigationDelegate
|
||||
|
||||
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)
|
||||
|
||||
|
@ -213,15 +210,12 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
}
|
||||
|
||||
// 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 = false
|
||||
pollView.mastodonController = mastodonController
|
||||
pollView.updateUI(status: status, poll: poll)
|
||||
} else {
|
||||
pollView.isHidden = true
|
||||
}
|
||||
pollView.isHidden = status.poll == nil
|
||||
pollView.mastodonController = mastodonController
|
||||
pollView.updateUI(status: status, poll: status.poll)
|
||||
}
|
||||
|
||||
func updateUI(account: AccountMO) {
|
||||
|
@ -359,6 +353,7 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
contentTextView.isHidden = collapsed
|
||||
cardView.isHidden = cardView.card == nil || collapsed
|
||||
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
|
||||
pollView.isHidden = pollView.poll == nil || collapsed
|
||||
|
||||
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
|
||||
|
||||
|
|
|
@ -33,7 +33,21 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
|||
|
||||
profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: 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)
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
static let relativeDateFormatter: RelativeDateTimeFormatter = {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.dateTimeStyle = .numeric
|
||||
formatter.unitsStyle = .short
|
||||
formatter.unitsStyle = .full
|
||||
return formatter
|
||||
}()
|
||||
|
||||
|
@ -44,7 +44,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
|
||||
reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
|
||||
|
||||
accessibilityElements!.insert(reblogLabel!, at: 0)
|
||||
isAccessibilityElement = true
|
||||
|
||||
// todo: double check this on RTL layouts
|
||||
replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
|
||||
|
@ -59,8 +59,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
|
||||
if rebloggerAccountUpdater == nil {
|
||||
rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject
|
||||
.filter { [unowned self] in $0 == self.rebloggerID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.filter { [unowned self] in $0 == self.rebloggerID }
|
||||
.sink { [unowned self] in
|
||||
if let mastodonController = self.mastodonController,
|
||||
let reblogger = mastodonController.persistentContainer.account(for: $0) {
|
||||
|
@ -134,7 +134,6 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
|
||||
private func doUpdateTimestamp(status: StatusMO) {
|
||||
timestampLabel.text = status.createdAt.timeAgoString()
|
||||
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
|
||||
|
||||
let delay: DispatchTimeInterval?
|
||||
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 {
|
||||
|
@ -295,8 +326,11 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
|||
|
||||
extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
||||
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),
|
||||
let accountID = mastodonController.accountInfo?.id else {
|
||||
let accountID = mastodonController.accountInfo?.id,
|
||||
!pollView.isTracking else {
|
||||
return []
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?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"/>
|
||||
<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="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -38,7 +39,7 @@
|
|||
</constraints>
|
||||
</imageView>
|
||||
<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>
|
||||
<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"/>
|
||||
|
@ -132,13 +133,19 @@
|
|||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</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>
|
||||
</stackView>
|
||||
<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"/>
|
||||
<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">
|
||||
<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"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
|
||||
<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 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="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="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"/>
|
||||
|
@ -276,7 +283,7 @@
|
|||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||
<image name="globe" catalog="system" width="128" height="121"/>
|
||||
<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"/>
|
||||
<systemColor name="labelColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
|
|
|
@ -55,6 +55,8 @@ class VisualEffectImageButton: UIControl {
|
|||
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
|
||||
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
@objc private func onTap() {
|
||||
|
|
Loading…
Reference in New Issue