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
|
# Changelog
|
||||||
|
|
||||||
|
## 2021.1 (20)
|
||||||
|
This is a big one! In addition to a bunch of fixes for anyone on the iOS 15 beta, there are a couple of big ticket features, including the Open in Tusker action extension and the Disable Infinite Scrolling preference.
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- Add Open in Tusker action extension
|
||||||
|
- Quickly search for any URL in Tusker
|
||||||
|
- In a share sheet, scroll to the bottom, tap "Edit Actions..." and turn on the "Open in Tusker" action
|
||||||
|
- Add Digital Wellness preference to disable infinite scrolling
|
||||||
|
- Add fast account switching indicator to My Profile tab
|
||||||
|
- Improve VoiceOver accessibility of polls and timeline statuses
|
||||||
|
- iPadOS: Create multiple main windows for different accounts by dragging from an account in Preferences
|
||||||
|
- iPadOS: Delete attachments on Compose screen by right-clicking and selecting Delete
|
||||||
|
- iPadOS 15: Add Open in New Window context menu action to most things
|
||||||
|
- iPadOS 15: Allow dragging the Compose sheet into a separate window
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix being unable to commit previewed account from timeline status
|
||||||
|
- Fix crash when searching fails
|
||||||
|
- Fix poll option percentages being cut off
|
||||||
|
- Fix polls not collapsing inside CWs
|
||||||
|
- Fix More button on profiles not being accessible with VoiceOver
|
||||||
|
- Fix VoiceOver reading profile fields in incorrect order
|
||||||
|
- Fix gallery animations jittering on devices with square screens (iPads, non-notched iPhones)
|
||||||
|
- Fix CW text jumping around post collapse animation
|
||||||
|
- iOS 15: Fix crash due when showing Draw Something screen in Compose
|
||||||
|
- iPadOS 14/iOS 15: Fix navigation bar turning transparent after opening the attachment gallery
|
||||||
|
- iPadOS 14/iOS 15: Fix drag-selecting poll options initiating a status cell drag interaction
|
||||||
|
- iPadOS: Fix crash when loading a previously-opened conversation window
|
||||||
|
- iPadOS 15: Fix showing Compose screen when keyboard focus moves through the sidebar
|
||||||
|
|
||||||
|
Known Issues:
|
||||||
|
- Disable Infinite Scrolling preference only affects timelines, not notifications or profiles
|
||||||
|
- iPadOS 15: The Compose sheet cannot be dismissed by swiping down
|
||||||
|
- iPadOS 15: Keyboard focus is stuck in the sidebar
|
||||||
|
|
||||||
## 2021.1 (19)
|
## 2021.1 (19)
|
||||||
This is an emergency fix for Tusker breaking when connecting to Mastodon instances on 3.4.0rc1.
|
This is an emergency fix for Tusker breaking when connecting to Mastodon instances on 3.4.0rc1.
|
||||||
|
|
||||||
|
|
|
@ -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 */; };
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
||||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
||||||
|
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */; };
|
||||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
|
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
|
||||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
||||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
||||||
|
@ -186,6 +187,7 @@
|
||||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
|
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
|
||||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
|
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
|
||||||
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */; };
|
||||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
||||||
|
@ -303,10 +305,17 @@
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
||||||
|
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */; };
|
||||||
|
D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */; };
|
||||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
||||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||||
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
|
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
||||||
|
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
|
||||||
|
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
|
||||||
|
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
|
||||||
|
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; };
|
||||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; };
|
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; };
|
||||||
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
|
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
|
||||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
||||||
|
@ -363,9 +372,27 @@
|
||||||
remoteGlobalIDString = D6D4DDCB212518A000E1C4BB;
|
remoteGlobalIDString = D6D4DDCB212518A000E1C4BB;
|
||||||
remoteInfo = Tusker;
|
remoteInfo = Tusker;
|
||||||
};
|
};
|
||||||
|
D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = D6E343A7265AAD6B00C4AA01;
|
||||||
|
remoteInfo = OpenInTusker;
|
||||||
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
D6E3438F2659849800C4AA01 /* Embed App Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed App Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D6F953E52125197500CF0F2B /* Embed Frameworks */ = {
|
D6F953E52125197500CF0F2B /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -388,7 +415,6 @@
|
||||||
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
||||||
0461A38F2163CBAE00C0A807 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
04D14BAE22B34A2800642648 /* GalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewController.swift; sourceTree = "<group>"; };
|
04D14BAE22B34A2800642648 /* GalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewController.swift; sourceTree = "<group>"; };
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||||
|
@ -530,6 +556,7 @@
|
||||||
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
||||||
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; };
|
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; };
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
||||||
|
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableTimelineLikeTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; };
|
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; };
|
||||||
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -565,6 +592,7 @@
|
||||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
||||||
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
|
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
|
||||||
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||||
|
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherIndicatorView.swift; sourceTree = "<group>"; };
|
||||||
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
||||||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -683,10 +711,19 @@
|
||||||
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
||||||
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
||||||
|
D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfirmLoadMoreTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
|
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
|
||||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
|
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
|
||||||
|
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
|
||||||
|
D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6E343AF265AAD6B00C4AA01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||||
|
D6E343B1265AAD6B00C4AA01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
D6E343B5265AAD6B00C4AA01 /* OpenInTusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInTusker.entitlements; sourceTree = "<group>"; };
|
||||||
|
D6E343B9265AAD8C00C4AA01 /* Action.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = "<group>"; };
|
||||||
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAutocompleteView.swift; sourceTree = "<group>"; };
|
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAutocompleteView.swift; sourceTree = "<group>"; };
|
||||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcher.swift; sourceTree = "<group>"; };
|
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcher.swift; sourceTree = "<group>"; };
|
||||||
D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcherTests.swift; sourceTree = "<group>"; };
|
D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcherTests.swift; sourceTree = "<group>"; };
|
||||||
|
@ -752,6 +789,13 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D6E343A5265AAD6B00C4AA01 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
@ -1413,6 +1457,7 @@
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||||
|
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||||
D626494023C122C800612E6E /* Asset Picker */,
|
D626494023C122C800612E6E /* Asset Picker */,
|
||||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||||
|
@ -1424,6 +1469,7 @@
|
||||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||||
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
|
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
|
||||||
D61AC1DA232EA43100C54D2D /* Instance Cell */,
|
D61AC1DA232EA43100C54D2D /* Instance Cell */,
|
||||||
|
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1446,6 +1492,7 @@
|
||||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||||
|
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1466,12 +1513,12 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6BC874421961F73006163F1 /* Gifu.framework */,
|
D6BC874421961F73006163F1 /* Gifu.framework */,
|
||||||
0461A38F2163CBAE00C0A807 /* Cache.framework */,
|
|
||||||
D61099AC2144B0CC00432DC2 /* Pachyderm */,
|
D61099AC2144B0CC00432DC2 /* Pachyderm */,
|
||||||
D61099B92144B0CC00432DC2 /* PachydermTests */,
|
D61099B92144B0CC00432DC2 /* PachydermTests */,
|
||||||
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
||||||
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
||||||
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
||||||
|
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */,
|
||||||
D6D4DDCD212518A000E1C4BB /* Products */,
|
D6D4DDCD212518A000E1C4BB /* Products */,
|
||||||
D65A37F221472F300087646E /* Frameworks */,
|
D65A37F221472F300087646E /* Frameworks */,
|
||||||
);
|
);
|
||||||
|
@ -1485,6 +1532,7 @@
|
||||||
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */,
|
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */,
|
||||||
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */,
|
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */,
|
||||||
D61099B32144B0CC00432DC2 /* PachydermTests.xctest */,
|
D61099B32144B0CC00432DC2 /* PachydermTests.xctest */,
|
||||||
|
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1552,6 +1600,28 @@
|
||||||
path = TuskerUITests;
|
path = TuskerUITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */,
|
||||||
|
D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */,
|
||||||
|
);
|
||||||
|
path = "Confirm Load More Cell";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6E343B5265AAD6B00C4AA01 /* OpenInTusker.entitlements */,
|
||||||
|
D6E343AA265AAD6B00C4AA01 /* Media.xcassets */,
|
||||||
|
D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */,
|
||||||
|
D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */,
|
||||||
|
D6E343B1265AAD6B00C4AA01 /* Info.plist */,
|
||||||
|
D6E343B9265AAD8C00C4AA01 /* Action.js */,
|
||||||
|
);
|
||||||
|
path = OpenInTusker;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1642,11 +1712,13 @@
|
||||||
D6D4DDCA212518A000E1C4BB /* Resources */,
|
D6D4DDCA212518A000E1C4BB /* Resources */,
|
||||||
D6F953E52125197500CF0F2B /* Embed Frameworks */,
|
D6F953E52125197500CF0F2B /* Embed Frameworks */,
|
||||||
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
|
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
|
||||||
|
D6E3438F2659849800C4AA01 /* Embed App Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
D61099BF2144B0CC00432DC2 /* PBXTargetDependency */,
|
D61099BF2144B0CC00432DC2 /* PBXTargetDependency */,
|
||||||
|
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Tusker;
|
name = Tusker;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
@ -1696,14 +1768,31 @@
|
||||||
productReference = D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */;
|
productReference = D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.ui-testing";
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
};
|
};
|
||||||
|
D6E343A7265AAD6B00C4AA01 /* OpenInTusker */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = D6E343B6265AAD6B00C4AA01 /* Build configuration list for PBXNativeTarget "OpenInTusker" */;
|
||||||
|
buildPhases = (
|
||||||
|
D6E343A4265AAD6B00C4AA01 /* Sources */,
|
||||||
|
D6E343A5265AAD6B00C4AA01 /* Frameworks */,
|
||||||
|
D6E343A6265AAD6B00C4AA01 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = OpenInTusker;
|
||||||
|
productName = OpenInTusker;
|
||||||
|
productReference = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
D6D4DDC4212518A000E1C4BB /* Project object */ = {
|
D6D4DDC4212518A000E1C4BB /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 1200;
|
LastSwiftUpdateCheck = 1250;
|
||||||
LastUpgradeCheck = 1020;
|
LastUpgradeCheck = 1250;
|
||||||
ORGANIZATIONNAME = Shadowfacts;
|
ORGANIZATIONNAME = Shadowfacts;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
D61099AA2144B0CC00432DC2 = {
|
D61099AA2144B0CC00432DC2 = {
|
||||||
|
@ -1729,6 +1818,9 @@
|
||||||
LastSwiftMigration = 1020;
|
LastSwiftMigration = 1020;
|
||||||
TestTargetID = D6D4DDCB212518A000E1C4BB;
|
TestTargetID = D6D4DDCB212518A000E1C4BB;
|
||||||
};
|
};
|
||||||
|
D6E343A7265AAD6B00C4AA01 = {
|
||||||
|
CreatedOnToolsVersion = 12.5;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = D6D4DDC7212518A000E1C4BB /* Build configuration list for PBXProject "Tusker" */;
|
buildConfigurationList = D6D4DDC7212518A000E1C4BB /* Build configuration list for PBXProject "Tusker" */;
|
||||||
|
@ -1760,6 +1852,7 @@
|
||||||
D6D4DDEA212518A200E1C4BB /* TuskerUITests */,
|
D6D4DDEA212518A200E1C4BB /* TuskerUITests */,
|
||||||
D61099AA2144B0CC00432DC2 /* Pachyderm */,
|
D61099AA2144B0CC00432DC2 /* Pachyderm */,
|
||||||
D61099B22144B0CC00432DC2 /* PachydermTests */,
|
D61099B22144B0CC00432DC2 /* PachydermTests */,
|
||||||
|
D6E343A7265AAD6B00C4AA01 /* OpenInTusker */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
@ -1810,6 +1903,7 @@
|
||||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||||
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
|
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
|
||||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
||||||
|
D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1827,6 +1921,16 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D6E343A6265AAD6B00C4AA01 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */,
|
||||||
|
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */,
|
||||||
|
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
@ -1933,6 +2037,7 @@
|
||||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
||||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||||
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
|
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
|
||||||
|
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */,
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||||
|
@ -2065,6 +2170,7 @@
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
|
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
|
||||||
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
|
@ -2085,6 +2191,7 @@
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
|
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
|
@ -2170,6 +2277,14 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D6E343A4265AAD6B00C4AA01 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
|
@ -2198,6 +2313,11 @@
|
||||||
target = D6D4DDCB212518A000E1C4BB /* Tusker */;
|
target = D6D4DDCB212518A000E1C4BB /* Tusker */;
|
||||||
targetProxy = D6D4DDEC212518A200E1C4BB /* PBXContainerItemProxy */;
|
targetProxy = D6D4DDEC212518A200E1C4BB /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */;
|
||||||
|
targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
|
@ -2209,6 +2329,14 @@
|
||||||
name = LaunchScreen.storyboard;
|
name = LaunchScreen.storyboard;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
D6E343AF265AAD6B00C4AA01 /* Base */,
|
||||||
|
);
|
||||||
|
name = MainInterface.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */ = {
|
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2234,6 +2362,7 @@
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
INFOPLIST_FILE = Pachyderm/Info.plist;
|
INFOPLIST_FILE = Pachyderm/Info.plist;
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2264,6 +2393,7 @@
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
INFOPLIST_FILE = Pachyderm/Info.plist;
|
INFOPLIST_FILE = Pachyderm/Info.plist;
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2346,6 +2476,7 @@
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
@ -2408,6 +2539,7 @@
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
@ -2445,11 +2577,11 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 19;
|
CURRENT_PROJECT_VERSION = 20;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2461,6 +2593,10 @@
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphoneos15.0]" = "SDK_IOS_15 $(inherited)";
|
||||||
|
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphonesimulator15.0]" = "SDK_IOS_15 $(inherited)";
|
||||||
|
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=macosx12.0]" = "SDK_IOS_15 $(inherited)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||||
};
|
};
|
||||||
|
@ -2474,11 +2610,11 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 19;
|
CURRENT_PROJECT_VERSION = 20;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2576,6 +2712,60 @@
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
D6E343B7265AAD6B00C4AA01 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 2021.1;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
D6E343B8265AAD6B00C4AA01 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 2021.1;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
@ -2633,6 +2823,15 @@
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
D6E343B6265AAD6B00C4AA01 /* Build configuration list for PBXNativeTarget "OpenInTusker" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
D6E343B7265AAD6B00C4AA01 /* Debug */,
|
||||||
|
D6E343B8265AAD6B00C4AA01 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
@ -2649,7 +2848,7 @@
|
||||||
repositoryURL = "https://github.com/microsoft/plcrashreporter";
|
repositoryURL = "https://github.com/microsoft/plcrashreporter";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMinorVersion;
|
kind = upToNextMinorVersion;
|
||||||
minimumVersion = 1.7.0;
|
minimumVersion = 1.8.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = {
|
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1020"
|
LastUpgradeVersion = "1250"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -29,8 +29,6 @@
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
</Testables>
|
</Testables>
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
@ -51,8 +49,6 @@
|
||||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
ReferencedContainer = "container:Tusker.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1020"
|
LastUpgradeVersion = "1250"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -88,6 +88,10 @@
|
||||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-UIFocusLoopDebuggerEnabled YES"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</CommandLineArgument>
|
||||||
</CommandLineArguments>
|
</CommandLineArguments>
|
||||||
<EnvironmentVariables>
|
<EnvironmentVariables>
|
||||||
<EnvironmentVariable
|
<EnvironmentVariable
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "6b7ca9a2faad6ea990ff60b0a3ee4fdf3db59150",
|
"revision": "de6b8f9db4b2a0aa859a5507550a70548e4da936",
|
||||||
"version": "1.7.2"
|
"version": "1.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -65,6 +65,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
|
case .mainScene:
|
||||||
|
return "main-scene"
|
||||||
|
|
||||||
case .showConversation,
|
case .showConversation,
|
||||||
.showTimeline,
|
.showTimeline,
|
||||||
.checkNotifications,
|
.checkNotifications,
|
||||||
|
@ -76,9 +79,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
case .newPost:
|
case .newPost:
|
||||||
return "compose"
|
return "compose"
|
||||||
|
|
||||||
default:
|
|
||||||
fatalError("no scene for activity type \(type)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,21 +17,39 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard LocalData.shared.onboardingComplete else {
|
||||||
|
UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let account: LocalData.UserAccountInfo
|
let account: LocalData.UserAccountInfo
|
||||||
|
let controller: MastodonController
|
||||||
let draft: Draft?
|
let draft: Draft?
|
||||||
|
|
||||||
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity,
|
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||||
let activityAccount = UserActivityManager.getAccount(from: activity) {
|
if let activityAccount = UserActivityManager.getAccount(from: activity) {
|
||||||
account = activityAccount
|
account = activityAccount
|
||||||
draft = UserActivityManager.getDraft(from: activity)
|
} else {
|
||||||
|
// todo: this potentially changes the account for the draft, should show the same warning to user as in the drafts selection screen
|
||||||
|
account = LocalData.shared.getMostRecentAccount()!
|
||||||
|
}
|
||||||
|
|
||||||
|
controller = MastodonController.getForAccount(account)
|
||||||
|
|
||||||
|
if let activityDraft = UserActivityManager.getDraft(from: activity) {
|
||||||
|
draft = activityDraft
|
||||||
|
} else if let mentioning = activity.userInfo?["mentioning"] as? String {
|
||||||
|
draft = controller.createDraft(inReplyToID: nil, mentioningAcct: mentioning)
|
||||||
|
} else {
|
||||||
|
draft = nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
account = LocalData.shared.getMostRecentAccount()!
|
account = LocalData.shared.getMostRecentAccount()!
|
||||||
|
controller = MastodonController.getForAccount(account)
|
||||||
draft = nil
|
draft = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = MastodonController.getForAccount(account)
|
|
||||||
session.mastodonController = controller
|
session.mastodonController = controller
|
||||||
|
|
||||||
controller.getOwnAccount()
|
controller.getOwnAccount()
|
||||||
controller.getOwnInstance()
|
controller.getOwnInstance()
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.bookmarks</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.bookmarks</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.main-scene</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
|
@ -15,12 +15,15 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
|
private var launchActivity: NSUserActivity?
|
||||||
|
|
||||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
|
||||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
|
||||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
|
||||||
guard let windowScene = scene as? UIWindowScene else { return }
|
guard let windowScene = scene as? UIWindowScene else { return }
|
||||||
|
|
||||||
|
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||||
|
launchActivity = activity
|
||||||
|
}
|
||||||
|
|
||||||
window = UIWindow(windowScene: windowScene)
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
|
||||||
if let report = AppDelegate.pendingCrashReport {
|
if let report = AppDelegate.pendingCrashReport {
|
||||||
|
@ -28,6 +31,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
handlePendingCrashReport(report, session: session)
|
handlePendingCrashReport(report, session: session)
|
||||||
} else {
|
} else {
|
||||||
showAppOrOnboardingUI(session: session)
|
showAppOrOnboardingUI(session: session)
|
||||||
|
if connectionOptions.urlContexts.count > 0 {
|
||||||
|
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window!.makeKeyAndVisible()
|
window!.makeKeyAndVisible()
|
||||||
|
@ -50,20 +56,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
if url.host == "x-callback-url" {
|
if url.host == "x-callback-url" {
|
||||||
_ = XCBManager.handle(url: url)
|
_ = XCBManager.handle(url: url)
|
||||||
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||||
let tabBarController = window!.rootViewController as? MainTabBarViewController,
|
let rootViewController = window!.rootViewController as? TuskerRootViewController {
|
||||||
let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController,
|
|
||||||
let exploreController = exploreNavController.viewControllers.first as? ExploreViewController {
|
|
||||||
|
|
||||||
tabBarController.select(tab: .explore)
|
|
||||||
exploreNavController.popToRootViewController(animated: false)
|
|
||||||
|
|
||||||
exploreController.loadViewIfNeeded()
|
|
||||||
exploreController.searchController.isActive = true
|
|
||||||
|
|
||||||
components.scheme = "https"
|
components.scheme = "https"
|
||||||
let query = url.absoluteString
|
let query = components.string!
|
||||||
exploreController.searchController.searchBar.text = query
|
rootViewController.performSearch(query: query)
|
||||||
exploreController.resultsController.performSearch(query: query)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +94,14 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
DraftsManager.save()
|
DraftsManager.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
||||||
|
if let mastodonController = window?.windowScene?.session.mastodonController {
|
||||||
|
return UserActivityManager.mainSceneActivity(accountID: mastodonController.accountInfo!.id)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||||
// Called as the scene transitions from the background to the foreground.
|
// Called as the scene transitions from the background to the foreground.
|
||||||
// Use this method to undo the changes made on entering the background.
|
// Use this method to undo the changes made on entering the background.
|
||||||
|
@ -130,7 +134,14 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
|
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
|
||||||
let session = session ?? window!.windowScene!.session
|
let session = session ?? window!.windowScene!.session
|
||||||
if LocalData.shared.onboardingComplete {
|
if LocalData.shared.onboardingComplete {
|
||||||
let account = LocalData.shared.getMostRecentAccount()!
|
let account: LocalData.UserAccountInfo
|
||||||
|
if let activity = launchActivity,
|
||||||
|
let activityAccount = UserActivityManager.getAccount(from: activity) {
|
||||||
|
account = activityAccount
|
||||||
|
} else {
|
||||||
|
account = LocalData.shared.getMostRecentAccount()!
|
||||||
|
}
|
||||||
|
|
||||||
if session.mastodonController == nil {
|
if session.mastodonController == nil {
|
||||||
session.mastodonController = MastodonController.getForAccount(account)
|
session.mastodonController = MastodonController.getForAccount(account)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import MobileCoreServices
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
||||||
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
||||||
|
@ -52,10 +52,10 @@ final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
||||||
|
|
||||||
extension CompositionAttachment: Identifiable {}
|
extension CompositionAttachment: Identifiable {}
|
||||||
|
|
||||||
private let imageType = kUTTypeImage as String
|
private let imageType = UTType.image.identifier
|
||||||
private let mp4Type = kUTTypeMPEG4 as String
|
private let mp4Type = UTType.mpeg4Movie.identifier
|
||||||
private let quickTimeType = kUTTypeQuickTimeMovie as String
|
private let quickTimeType = UTType.quickTimeMovie.identifier
|
||||||
private let dataType = kUTTypeData as String
|
private let dataType = UTType.data.identifier
|
||||||
|
|
||||||
extension CompositionAttachment: NSItemProviderWriting {
|
extension CompositionAttachment: NSItemProviderWriting {
|
||||||
static var writableTypeIdentifiersForItemProvider: [String] {
|
static var writableTypeIdentifiersForItemProvider: [String] {
|
||||||
|
@ -100,11 +100,11 @@ extension CompositionAttachment: NSItemProviderReading {
|
||||||
return try PropertyListDecoder().decode(Self.self, from: data)
|
return try PropertyListDecoder().decode(Self.self, from: data)
|
||||||
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
|
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
|
||||||
return CompositionAttachment(data: .image(image)) as! Self
|
return CompositionAttachment(data: .image(image)) as! Self
|
||||||
} else if typeIdentifier == mp4Type || typeIdentifier == quickTimeType {
|
} else if let type = UTType(typeIdentifier), type == .mpeg4Movie || type == .quickTimeMovie {
|
||||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
let temporaryFileName = ProcessInfo().globallyUniqueString
|
let temporaryFileName = ProcessInfo().globallyUniqueString
|
||||||
let fileExt = UTTypeCopyPreferredTagWithClass(typeIdentifier as CFString, kUTTagClassFilenameExtension)!
|
let fileExt = type.preferredFilenameExtension!
|
||||||
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt.takeUnretainedValue() as String)
|
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt)
|
||||||
try data.write(to: temporaryFileURL)
|
try data.write(to: temporaryFileURL)
|
||||||
return CompositionAttachment(data: .video(temporaryFileURL)) as! Self
|
return CompositionAttachment(data: .video(temporaryFileURL)) as! Self
|
||||||
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {
|
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Photos
|
import Photos
|
||||||
import MobileCoreServices
|
import UniformTypeIdentifiers
|
||||||
import PencilKit
|
import PencilKit
|
||||||
|
|
||||||
enum CompositionAttachmentData {
|
enum CompositionAttachmentData {
|
||||||
|
@ -74,7 +74,7 @@ enum CompositionAttachmentData {
|
||||||
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
||||||
mimeType = "image/jpeg"
|
mimeType = "image/jpeg"
|
||||||
} else {
|
} else {
|
||||||
mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String
|
mimeType = UTType(dataUTI)!.preferredMIMEType!
|
||||||
}
|
}
|
||||||
|
|
||||||
completion(data, mimeType)
|
completion(data, mimeType)
|
||||||
|
|
|
@ -63,6 +63,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
||||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||||
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
||||||
|
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
||||||
|
|
||||||
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
||||||
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
||||||
|
@ -97,6 +98,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
||||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||||
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
||||||
|
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
||||||
|
|
||||||
try container.encode(silentActions, forKey: .silentActions)
|
try container.encode(silentActions, forKey: .silentActions)
|
||||||
try container.encode(statusContentType, forKey: .statusContentType)
|
try container.encode(statusContentType, forKey: .statusContentType)
|
||||||
|
@ -133,6 +135,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var showFavoriteAndReblogCounts = true
|
@Published var showFavoriteAndReblogCounts = true
|
||||||
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
||||||
@Published var grayscaleImages = false
|
@Published var grayscaleImages = false
|
||||||
|
@Published var disableInfiniteScrolling = false
|
||||||
|
|
||||||
// MARK: Advanced
|
// MARK: Advanced
|
||||||
@Published var silentActions: [String: Permission] = [:]
|
@Published var silentActions: [String: Permission] = [:]
|
||||||
|
@ -165,6 +168,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case showFavoriteAndReblogCounts
|
case showFavoriteAndReblogCounts
|
||||||
case defaultNotificationsType
|
case defaultNotificationsType
|
||||||
case grayscaleImages
|
case grayscaleImages
|
||||||
|
case disableInfiniteScrolling
|
||||||
|
|
||||||
case silentActions
|
case silentActions
|
||||||
case statusContentType
|
case statusContentType
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Photos
|
||||||
private let reuseIdentifier = "assetCell"
|
private let reuseIdentifier = "assetCell"
|
||||||
private let cameraReuseIdentifier = "showCameraCell"
|
private let cameraReuseIdentifier = "showCameraCell"
|
||||||
|
|
||||||
protocol AssetCollectionViewControllerDelegate: class {
|
protocol AssetCollectionViewControllerDelegate: AnyObject {
|
||||||
func shouldSelectAsset(_ asset: PHAsset) -> Bool
|
func shouldSelectAsset(_ asset: PHAsset) -> Bool
|
||||||
func didSelectAssets(_ assets: [PHAsset])
|
func didSelectAssets(_ assets: [PHAsset])
|
||||||
func captureFromCamera()
|
func captureFromCamera()
|
||||||
|
@ -70,6 +70,7 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
|
|
||||||
collectionView.alwaysBounceVertical = true
|
collectionView.alwaysBounceVertical = true
|
||||||
collectionView.allowsMultipleSelection = true
|
collectionView.allowsMultipleSelection = true
|
||||||
|
collectionView.allowsSelection = true
|
||||||
|
|
||||||
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
||||||
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
||||||
|
@ -98,8 +99,6 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setEditing(true, animated: false)
|
|
||||||
|
|
||||||
updateItemsSelectedCount()
|
updateItemsSelectedCount()
|
||||||
|
|
||||||
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {
|
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Photos
|
import Photos
|
||||||
|
|
||||||
protocol AssetPickerViewControllerDelegate: class {
|
protocol AssetPickerViewControllerDelegate: AnyObject {
|
||||||
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool
|
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool
|
||||||
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData])
|
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData])
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,8 +51,14 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
}
|
}
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
|
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||||
|
didSet {
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
override var prefersStatusBarHidden: Bool {
|
||||||
return true
|
return !isInteractivelyAnimatingDismissal
|
||||||
}
|
}
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||||
return .none
|
return .none
|
||||||
|
|
|
@ -41,6 +41,16 @@ struct ComposeAttachmentRow: View {
|
||||||
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
Button(action: self.removeAttachment) {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}.foregroundStyle(.red)
|
||||||
|
} else {
|
||||||
|
Button(action: self.removeAttachment) {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
|
@ -126,7 +136,9 @@ struct ComposeAttachmentRow: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeAttachment() {
|
private func removeAttachment() {
|
||||||
draft.attachments.removeAll { $0.id == attachment.id }
|
withAnimation {
|
||||||
|
draft.attachments.removeAll { $0.id == attachment.id }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func editDrawing() {
|
private func editDrawing() {
|
||||||
|
|
|
@ -69,6 +69,7 @@ struct ComposeAttachmentsList: View {
|
||||||
.frame(height: cellHeight / 2)
|
.frame(height: cellHeight / 2)
|
||||||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||||
}
|
}
|
||||||
|
.listStyle(PlainListStyle())
|
||||||
.frame(height: totalListHeight)
|
.frame(height: totalListHeight)
|
||||||
.onAppear(perform: self.didAppear)
|
.onAppear(perform: self.didAppear)
|
||||||
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import PencilKit
|
import PencilKit
|
||||||
|
|
||||||
protocol ComposeDrawingViewControllerDelegate: class {
|
protocol ComposeDrawingViewControllerDelegate: AnyObject {
|
||||||
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
|
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
|
||||||
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)
|
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ class ComposeDrawingViewController: UIViewController {
|
||||||
private(set) var undoBarButtonItem: UIBarButtonItem!
|
private(set) var undoBarButtonItem: UIBarButtonItem!
|
||||||
private(set) var redoBarButtonItem: UIBarButtonItem!
|
private(set) var redoBarButtonItem: UIBarButtonItem!
|
||||||
|
|
||||||
|
private var toolPicker: PKToolPicker!
|
||||||
|
|
||||||
private var initialDrawing: PKDrawing?
|
private var initialDrawing: PKDrawing?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -70,26 +72,21 @@ class ComposeDrawingViewController: UIViewController {
|
||||||
canvasView.topAnchor.constraint(equalTo: view.topAnchor),
|
canvasView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
toolPicker = PKToolPicker()
|
||||||
|
toolPicker.setVisible(true, forFirstResponder: canvasView)
|
||||||
|
toolPicker.addObserver(canvasView)
|
||||||
|
toolPicker.addObserver(self)
|
||||||
|
|
||||||
|
updateLayout(for: toolPicker)
|
||||||
|
canvasView.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
// todo: should the PKToolPicker be owned by this VC or something else?
|
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidUndoChange, object: self.undoManager!)
|
||||||
if let window = parent?.view.window, let toolPicker = PKToolPicker.shared(for: window) {
|
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidRedoChange, object: self.undoManager!)
|
||||||
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!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(for toolPicker: PKToolPicker) {
|
func updateLayout(for toolPicker: PKToolPicker) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import PencilKit
|
import PencilKit
|
||||||
|
|
||||||
protocol ComposeHostingControllerDelegate: class {
|
protocol ComposeHostingControllerDelegate: AnyObject {
|
||||||
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,6 +245,25 @@ extension ComposeHostingController: ComposeUIStateDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentAssetPickerSheet() {
|
func presentAssetPickerSheet() {
|
||||||
|
#if SDK_IOS_15
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
let picker = AssetPickerViewController()
|
||||||
|
picker.assetPickerDelegate = self
|
||||||
|
picker.modalPresentationStyle = .pageSheet
|
||||||
|
picker.overrideUserInterfaceStyle = .dark
|
||||||
|
let sheet = picker.sheetPresentationController!
|
||||||
|
sheet.detents = [.medium(), .large()]
|
||||||
|
sheet.prefersEdgeAttachedInCompactHeight = true
|
||||||
|
self.present(picker, animated: true)
|
||||||
|
} else {
|
||||||
|
presentOldAssetPickerSheet()
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
presentOldAssetPickerSheet()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentOldAssetPickerSheet() {
|
||||||
let sheetContainer = AssetPickerSheetContainerViewController()
|
let sheetContainer = AssetPickerSheetContainerViewController()
|
||||||
sheetContainer.assetPicker.assetPickerDelegate = self
|
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||||
self.present(sheetContainer, animated: true)
|
self.present(sheetContainer, animated: true)
|
||||||
|
@ -288,7 +307,9 @@ extension ComposeHostingController: AssetPickerViewControllerDelegate {
|
||||||
let attachments = attachments.map {
|
let attachments = attachments.map {
|
||||||
CompositionAttachment(data: $0)
|
CompositionAttachment(data: $0)
|
||||||
}
|
}
|
||||||
draft.attachments.append(contentsOf: attachments)
|
withAnimation {
|
||||||
|
draft.attachments.append(contentsOf: attachments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ struct ComposePollView: View {
|
||||||
}
|
}
|
||||||
.accentColor(buttonForegroundColor)
|
.accentColor(buttonForegroundColor)
|
||||||
.background(Circle().foregroundColor(buttonBackgroundColor))
|
.background(Circle().foregroundColor(buttonBackgroundColor))
|
||||||
|
.hoverEffect()
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
|
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
|
||||||
|
@ -161,6 +162,7 @@ struct ComposePollOption: View {
|
||||||
}
|
}
|
||||||
.foregroundColor(poll.options.count == 1 ? .gray : .red)
|
.foregroundColor(poll.options.count == 1 ? .gray : .red)
|
||||||
.disabled(poll.options.count == 1)
|
.disabled(poll.options.count == 1)
|
||||||
|
.hoverEffect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,21 +23,21 @@ struct ComposeReplyView: View {
|
||||||
HStack(alignment: .top, spacing: horizSpacing) {
|
HStack(alignment: .top, spacing: horizSpacing) {
|
||||||
GeometryReader(content: self.replyAvatarImage)
|
GeometryReader(content: self.replyAvatarImage)
|
||||||
.frame(width: 50)
|
.frame(width: 50)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack {
|
HStack {
|
||||||
AccountDisplayNameLabel(account: status.account, fontSize: 17)
|
AccountDisplayNameLabel(account: status.account, fontSize: 17)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.layoutPriority(1)
|
.layoutPriority(1)
|
||||||
|
|
||||||
Text(verbatim: "@\(status.account.acct)")
|
Text(verbatim: "@\(status.account.acct)")
|
||||||
.font(.system(size: 17, weight: .light))
|
.font(.system(size: 17, weight: .light))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
ComposeReplyContentView(status: status) { (newHeight) in
|
ComposeReplyContentView(status: status) { (newHeight) in
|
||||||
self.contentHeight = newHeight
|
self.contentHeight = newHeight
|
||||||
}
|
}
|
||||||
|
@ -52,9 +52,20 @@ struct ComposeReplyView: View {
|
||||||
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
||||||
// using named coordinate spaces produces an incorrect scroll offset on iOS 13,
|
// using named coordinate spaces produces an incorrect scroll offset on iOS 13,
|
||||||
// so simply compare the geometry inside and outside the scroll view in the global coordinate space
|
// so simply compare the geometry inside and outside the scroll view in the global coordinate space
|
||||||
var scrollOffset = outerMinY - geometry.frame(in: .global).minY
|
let scrollOffset = outerMinY - geometry.frame(in: .global).minY
|
||||||
scrollOffset += stackPadding
|
|
||||||
let offset = min(max(scrollOffset, 0), geometry.size.height - 50 - stackPadding)
|
// add stackPadding so that the image is always at least stackPadding away from the top
|
||||||
|
var offset = scrollOffset + stackPadding
|
||||||
|
|
||||||
|
// offset can never be less than 0 (i.e., above the top of the in-reply-to content)
|
||||||
|
offset = max(offset, 0)
|
||||||
|
|
||||||
|
// subtract 50, because we care about where the bottom of the view is but the offset is relative to the top of the view
|
||||||
|
let maxOffset = (contentHeight ?? 0) - 50
|
||||||
|
|
||||||
|
// once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content
|
||||||
|
offset = min(offset, maxOffset)
|
||||||
|
|
||||||
return ComposeAvatarImageView(url: status.account.avatar)
|
return ComposeAvatarImageView(url: status.account.avatar)
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol ComposeTextViewCaretScrolling: class {
|
protocol ComposeTextViewCaretScrolling: AnyObject {
|
||||||
var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set }
|
var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
protocol ComposeUIStateDelegate: class {
|
protocol ComposeUIStateDelegate: AnyObject {
|
||||||
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
|
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
|
||||||
|
|
||||||
func dismissCompose(mode: ComposeUIState.DismissMode)
|
func dismissCompose(mode: ComposeUIState.DismissMode)
|
||||||
|
@ -60,6 +60,6 @@ extension ComposeUIState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol ComposeAutocompleteHandler: class {
|
protocol ComposeAutocompleteHandler: AnyObject {
|
||||||
func autocomplete(with string: String)
|
func autocomplete(with string: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,10 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
if postProgress > 0 {
|
||||||
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||||
|
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
||||||
|
}
|
||||||
|
|
||||||
autocompleteSuggestions
|
autocompleteSuggestions
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
private let reuseIdentifier = "EmojiCell"
|
private let reuseIdentifier = "EmojiCell"
|
||||||
|
|
||||||
protocol EmojiPickerCollectionViewControllerDelegate: class {
|
protocol EmojiPickerCollectionViewControllerDelegate: AnyObject {
|
||||||
func selectedEmoji(_ emoji: Emoji)
|
func selectedEmoji(_ emoji: Emoji)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,31 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
|
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
|
||||||
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||||
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
||||||
|
|
||||||
|
loadMainStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadMainStatus() {
|
||||||
|
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
||||||
|
self.mainStatusLoaded(mainStatus)
|
||||||
|
} else {
|
||||||
|
let request = Client.getStatus(id: mainStatusID)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .success(status, _):
|
||||||
|
let viewContext = self.mastodonController.persistentContainer.viewContext
|
||||||
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false, context: viewContext) { (statusMO) in
|
||||||
|
self.mainStatusLoaded(statusMO)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(_):
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mainStatusLoaded(_ mainStatus: StatusMO) {
|
||||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
@ -139,12 +163,10 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
guard let mainStatus = self.mastodonController.persistentContainer.status(for: self.mainStatusID) else {
|
|
||||||
fatalError("Missing cached status \(self.mainStatusID)")
|
|
||||||
}
|
|
||||||
let mainStatusInReplyToID = mainStatus.inReplyToID
|
let mainStatusInReplyToID = mainStatus.inReplyToID
|
||||||
mainStatus.incrementReferenceCount()
|
mainStatus.incrementReferenceCount()
|
||||||
|
|
||||||
|
// todo: it would be nice to cache these contexts
|
||||||
let request = Status.getContext(mainStatusID)
|
let request = Status.getContext(mainStatusID)
|
||||||
mastodonController.run(request) { response in
|
mastodonController.run(request) { response in
|
||||||
guard case let .success(context, _) = response else { fatalError() }
|
guard case let .success(context, _) = response else { fatalError() }
|
||||||
|
@ -304,7 +326,16 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func toggleVisibilityButtonPressed() {
|
@objc func toggleVisibilityButtonPressed() {
|
||||||
|
#if SDK_IOS_15
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
visibilityBarButtonItem.isSelected = !visibilityBarButtonItem.isSelected
|
||||||
|
showStatusesAutomatically = visibilityBarButtonItem.isSelected
|
||||||
|
} else {
|
||||||
|
showStatusesAutomatically = !showStatusesAutomatically
|
||||||
|
}
|
||||||
|
#else
|
||||||
showStatusesAutomatically = !showStatusesAutomatically
|
showStatusesAutomatically = !showStatusesAutomatically
|
||||||
|
#endif
|
||||||
|
|
||||||
let snapshot = dataSource.snapshot()
|
let snapshot = dataSource.snapshot()
|
||||||
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol DraftsTableViewControllerDelegate: class {
|
protocol DraftsTableViewControllerDelegate: AnyObject {
|
||||||
func draftSelectionCanceled()
|
func draftSelectionCanceled()
|
||||||
func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void)
|
func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void)
|
||||||
func draftSelected(_ draft: Draft)
|
func draftSelected(_ draft: Draft)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol FastAccountSwitcherViewControllerDelegate: class {
|
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,14 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
private var prevZoomScale: CGFloat?
|
private var prevZoomScale: CGFloat?
|
||||||
private var isGrayscale = false
|
private var isGrayscale = false
|
||||||
|
|
||||||
|
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||||
|
didSet {
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
override var prefersStatusBarHidden: Bool {
|
||||||
return true
|
return !isInteractivelyAnimatingDismissal
|
||||||
}
|
}
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||||
return .none
|
return .none
|
||||||
|
|
|
@ -43,8 +43,14 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
var animationGifData: Data? { largeImageVC?.animationGifData }
|
var animationGifData: Data? { largeImageVC?.animationGifData }
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
|
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||||
|
didSet {
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
override var prefersStatusBarHidden: Bool {
|
||||||
return true
|
return !isInteractivelyAnimatingDismissal
|
||||||
}
|
}
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||||
return .none
|
return .none
|
||||||
|
|
|
@ -15,6 +15,7 @@ protocol LargeImageAnimatableViewController: UIViewController {
|
||||||
var animationImage: UIImage? { get }
|
var animationImage: UIImage? { get }
|
||||||
var animationGifData: Data? { get }
|
var animationGifData: Data? { get }
|
||||||
var dismissInteractionController: LargeImageInteractionController? { get }
|
var dismissInteractionController: LargeImageInteractionController? { get }
|
||||||
|
var isInteractivelyAnimatingDismissal: Bool { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LargeImageAnimatableViewController {
|
extension LargeImageAnimatableViewController {
|
||||||
|
@ -74,7 +75,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
toVC.largeImageController?.contentView.isHidden = true
|
toVC.largeImageController?.contentView.isHidden = true
|
||||||
toVC.largeImageController?.setControlsVisible(false, animated: false)
|
toVC.largeImageController?.setControlsVisible(false, animated: false)
|
||||||
|
|
||||||
var finalFrameSize = finalVCFrame.inset(by: fromVC.view.safeAreaInsets).size
|
var finalFrameSize = finalVCFrame.inset(by: toVC.view.safeAreaInsets).size
|
||||||
let newWidth = finalFrameSize.width / image.size.width
|
let newWidth = finalFrameSize.width / image.size.width
|
||||||
let newHeight = finalFrameSize.height / image.size.height
|
let newHeight = finalFrameSize.height / image.size.height
|
||||||
if newHeight < newWidth {
|
if newHeight < newWidth {
|
||||||
|
|
|
@ -14,9 +14,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
||||||
var direction: CGFloat?
|
var direction: CGFloat?
|
||||||
|
|
||||||
var shouldCompleteTransition = false
|
var shouldCompleteTransition = false
|
||||||
private weak var viewController: UIViewController!
|
private weak var viewController: LargeImageAnimatableViewController!
|
||||||
|
|
||||||
init(viewController: UIViewController) {
|
init(viewController: LargeImageAnimatableViewController) {
|
||||||
super.init()
|
super.init()
|
||||||
self.viewController = viewController
|
self.viewController = viewController
|
||||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
||||||
|
@ -42,6 +42,7 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
||||||
viewController.dismiss(animated: true)
|
viewController.dismiss(animated: true)
|
||||||
case .changed:
|
case .changed:
|
||||||
shouldCompleteTransition = progress > 0.5 || velocity > 1000
|
shouldCompleteTransition = progress > 0.5 || velocity > 1000
|
||||||
|
viewController.isInteractivelyAnimatingDismissal = progress > 0.1
|
||||||
update(progress)
|
update(progress)
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
inProgress = false
|
inProgress = false
|
||||||
|
@ -59,4 +60,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func cancel() {
|
||||||
|
super.cancel()
|
||||||
|
viewController.isInteractivelyAnimatingDismissal = false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ class ListTimelineViewController: TimelineTableViewController {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
|
|
||||||
// todo: show loading indicator
|
// todo: show loading indicator
|
||||||
reloadInitialItems()
|
reloadInitial()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,14 +71,21 @@ extension AccountSwitchingContainerViewController {
|
||||||
|
|
||||||
extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
func presentCompose() {
|
func presentCompose() {
|
||||||
|
loadViewIfNeeded()
|
||||||
root.presentCompose()
|
root.presentCompose()
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(tab: MainTabBarViewController.Tab) {
|
func select(tab: MainTabBarViewController.Tab) {
|
||||||
|
loadViewIfNeeded()
|
||||||
root.select(tab: tab)
|
root.select(tab: tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||||
root.getTabController(tab: tab)
|
loadViewIfNeeded()
|
||||||
|
return root.getTabController(tab: tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
func performSearch(query: String) {
|
||||||
|
root.performSearch(query: query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
protocol MainSidebarViewControllerDelegate: class {
|
protocol MainSidebarViewControllerDelegate: AnyObject {
|
||||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
private(set) var previouslySelectedItem: Item?
|
private(set) var previouslySelectedItem: Item?
|
||||||
var selectedItem: Item? {
|
var selectedItem: Item? {
|
||||||
guard let indexPath = collectionView.indexPathsForSelectedItems?.first else {
|
guard let indexPath = collectionView?.indexPathsForSelectedItems?.first else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return dataSource.itemIdentifier(for: indexPath)
|
return dataSource.itemIdentifier(for: indexPath)
|
||||||
|
@ -448,6 +448,20 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 15.0, *)
|
||||||
|
func collectionView(_ collectionView: UICollectionView, selectionFollowsFocusForItemAt indexPath: IndexPath) -> Bool {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// don't immediately select items that present VCs when the they're focused, only when deliberately selected
|
||||||
|
switch item {
|
||||||
|
case .tab(.compose), .addList, .addSavedHashtag, .addSavedInstance:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSidebarViewController: UICollectionViewDragDelegate {
|
extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||||
|
|
|
@ -19,6 +19,10 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private var tabBarViewController: MainTabBarViewController!
|
private var tabBarViewController: MainTabBarViewController!
|
||||||
|
|
||||||
|
private var secondaryNavController: UINavigationController! {
|
||||||
|
viewController(for: .secondary) as? UINavigationController
|
||||||
|
}
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
return .portrait
|
return .portrait
|
||||||
|
@ -67,8 +71,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(item: MainSidebarViewController.Item) {
|
func select(item: MainSidebarViewController.Item) {
|
||||||
let nav = viewController(for: .secondary) as! UINavigationController
|
secondaryNavController.viewControllers = getOrCreateNavigationStack(item: item)
|
||||||
nav.viewControllers = getOrCreateNavigationStack(item: item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOrCreateNavigationStack(item: MainSidebarViewController.Item) -> [UIViewController] {
|
func getOrCreateNavigationStack(item: MainSidebarViewController.Item) -> [UIViewController] {
|
||||||
|
@ -109,8 +112,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
private func transferNavigationStack(from item: MainSidebarViewController.Item, to destination: UINavigationController, dropFirst: Bool = false, append: Bool = false) {
|
private func transferNavigationStack(from item: MainSidebarViewController.Item, to destination: UINavigationController, dropFirst: Bool = false, append: Bool = false) {
|
||||||
var itemNavStack: [UIViewController]
|
var itemNavStack: [UIViewController]
|
||||||
if item == sidebar.selectedItem {
|
if item == sidebar.selectedItem {
|
||||||
let detailNav = viewController(for: .secondary) as! UINavigationController
|
itemNavStack = secondaryNavController.viewControllers
|
||||||
itemNavStack = detailNav.viewControllers
|
|
||||||
} else {
|
} else {
|
||||||
itemNavStack = navigationStacks[item] ?? []
|
itemNavStack = navigationStacks[item] ?? []
|
||||||
navigationStacks.removeValue(forKey: item)
|
navigationStacks.removeValue(forKey: item)
|
||||||
|
@ -190,8 +192,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
// Make sure viewDidLoad is called so that the searchController/resultsController have been initialized
|
// Make sure viewDidLoad is called so that the searchController/resultsController have been initialized
|
||||||
explore.loadViewIfNeeded()
|
explore.loadViewIfNeeded()
|
||||||
|
|
||||||
let nav = viewController(for: .secondary) as! UINavigationController
|
let search = secondaryNavController.viewControllers.first as! SearchViewController
|
||||||
let search = nav.viewControllers.first as! SearchViewController
|
|
||||||
// Copy the search query from the search VC to the Explore VC's search controller.
|
// Copy the search query from the search VC to the Explore VC's search controller.
|
||||||
let query = search.searchController.searchBar.text ?? ""
|
let query = search.searchController.searchBar.text ?? ""
|
||||||
explore.searchController.searchBar.text = query
|
explore.searchController.searchBar.text = query
|
||||||
|
@ -318,9 +319,8 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) {
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) {
|
||||||
let nav = viewController(for: .secondary) as! UINavigationController
|
|
||||||
if let previous = sidebar.previouslySelectedItem {
|
if let previous = sidebar.previouslySelectedItem {
|
||||||
navigationStacks[previous] = nav.viewControllers
|
navigationStacks[previous] = secondaryNavController.viewControllers
|
||||||
}
|
}
|
||||||
select(item: item)
|
select(item: item)
|
||||||
}
|
}
|
||||||
|
@ -353,10 +353,17 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
|
|
||||||
extension MainSplitViewController: TuskerRootViewController {
|
extension MainSplitViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
||||||
nav.presentationController?.delegate = vc
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
present(nav, animated: true)
|
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) {
|
func select(tab: MainTabBarViewController.Tab) {
|
||||||
|
@ -366,8 +373,15 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
presentCompose()
|
presentCompose()
|
||||||
} else {
|
} else {
|
||||||
select(item: .tab(tab))
|
if presentedViewController != nil {
|
||||||
sidebar.select(item: .tab(tab), animated: false)
|
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 {
|
if tab == .compose {
|
||||||
return nil
|
return nil
|
||||||
} else if case .tab(tab) = sidebar.selectedItem {
|
} else if case .tab(tab) = sidebar.selectedItem {
|
||||||
return viewController(for: .secondary)
|
return secondaryNavController
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func performSearch(query: String) {
|
||||||
|
guard traitCollection.horizontalSizeClass != .compact else {
|
||||||
|
// ensure the tab bar VC is loaded
|
||||||
|
loadViewIfNeeded()
|
||||||
|
tabBarViewController.performSearch(query: query)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sidebar.selectedItem != .search {
|
||||||
|
select(item: .search)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryNavController.popToRootViewController(animated: false)
|
||||||
|
|
||||||
|
if searchViewController.isViewLoaded {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
searchViewController.searchController.isActive = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchViewController.searchControllerStatusOnAppearance = true
|
||||||
|
searchViewController.loadViewIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
searchViewController.searchController.searchBar.text = query
|
||||||
|
searchViewController.resultsController.performSearch(query: query)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: BackgroundableViewController {
|
extension MainSplitViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
|
|
||||||
|
@ -16,6 +15,9 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
private var composePlaceholder: UIViewController!
|
private var composePlaceholder: UIViewController!
|
||||||
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||||
|
|
||||||
|
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
||||||
|
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
||||||
|
|
||||||
var selectedTab: Tab {
|
var selectedTab: Tab {
|
||||||
return Tab(rawValue: selectedIndex)!
|
return Tab(rawValue: selectedIndex)!
|
||||||
}
|
}
|
||||||
|
@ -71,9 +73,62 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
tapRecognizer.cancelsTouchesInView = false
|
tapRecognizer.cancelsTouchesInView = false
|
||||||
tabBar.addGestureRecognizer(tapRecognizer)
|
tabBar.addGestureRecognizer(tapRecognizer)
|
||||||
|
|
||||||
|
if findMyProfileTabBarButton() != nil {
|
||||||
|
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
||||||
|
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(fastSwitcherIndicator)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
fastSwitcherIndicator.widthAnchor.constraint(equalToConstant: 10),
|
||||||
|
fastSwitcherIndicator.heightAnchor.constraint(equalToConstant: 12),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
tabBar.isSpringLoaded = true
|
tabBar.isSpringLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
repositionFastSwitcherIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
repositionFastSwitcherIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func repositionFastSwitcherIndicator() {
|
||||||
|
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
|
||||||
|
// using interfaceOrientation isn't ideal, but UITabBar buttons may lay out horizontally even in the compact size class
|
||||||
|
if traitCollection.horizontalSizeClass == .compact && interfaceOrientation.isPortrait {
|
||||||
|
fastSwitcherConstraints = [
|
||||||
|
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4),
|
||||||
|
// tab bar button image width is 30
|
||||||
|
fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
fastSwitcherConstraints = [
|
||||||
|
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor),
|
||||||
|
fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate(fastSwitcherConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findMyProfileTabBarButton() -> UIView? {
|
||||||
|
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
|
||||||
|
// sanity check that there is 1 button per VC
|
||||||
|
guard tabBarButtons.count == viewControllers!.count,
|
||||||
|
let myProfileButton = tabBarButtons.last else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return myProfileButton
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
|
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
|
||||||
fastAccountSwitcher.hide()
|
fastAccountSwitcher.hide()
|
||||||
}
|
}
|
||||||
|
@ -138,6 +193,8 @@ extension MainTabBarViewController {
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
|
// viewWControllers array is setup in viewDidLoad
|
||||||
|
loadViewIfNeeded()
|
||||||
return viewControllers![tab.rawValue]
|
return viewControllers![tab.rawValue]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,13 +202,9 @@ extension MainTabBarViewController {
|
||||||
|
|
||||||
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
|
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||||
// sanity check that there is 1 button per VC
|
|
||||||
guard tabBarButtons.count == viewControllers!.count,
|
|
||||||
let myProfileButton = tabBarButtons.last else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view)
|
let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view)
|
||||||
return myProfileButton.bounds.contains(locationInButton)
|
return myProfileButton.bounds.contains(locationInButton)
|
||||||
}
|
}
|
||||||
|
@ -159,19 +212,56 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
|
|
||||||
extension MainTabBarViewController: TuskerRootViewController {
|
extension MainTabBarViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
||||||
nav.presentationController?.delegate = vc
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
present(nav, animated: true)
|
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) {
|
func select(tab: Tab) {
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
presentCompose()
|
presentCompose()
|
||||||
} else {
|
} 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 {
|
extension MainTabBarViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -12,4 +12,5 @@ protocol TuskerRootViewController: UIViewController {
|
||||||
func presentCompose()
|
func presentCompose()
|
||||||
func select(tab: MainTabBarViewController.Tab)
|
func select(tab: MainTabBarViewController.Tab)
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
||||||
|
func performSearch(query: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
protocol InstanceSelectorTableViewControllerDelegate: class {
|
protocol InstanceSelectorTableViewControllerDelegate: AnyObject {
|
||||||
func didSelectInstance(url: URL)
|
func didSelectInstance(url: URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,9 @@ struct PreferencesView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.onDrag {
|
||||||
|
let activity = UserActivityManager.mainSceneActivity(accountID: account.id)
|
||||||
|
return NSItemProvider(object: activity)
|
||||||
}
|
}
|
||||||
}.onDelete { (indices: IndexSet) in
|
}.onDelete { (indices: IndexSet) in
|
||||||
var indices = indices
|
var indices = indices
|
||||||
|
|
|
@ -16,6 +16,7 @@ struct WellnessPrefsView: View {
|
||||||
showFavAndReblogCount
|
showFavAndReblogCount
|
||||||
notificationsMode
|
notificationsMode
|
||||||
grayscaleImages
|
grayscaleImages
|
||||||
|
disableInfiniteScrolling
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle(Text("Digital Wellness"))
|
.navigationBarTitle(Text("Digital Wellness"))
|
||||||
|
@ -46,6 +47,14 @@ struct WellnessPrefsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var disableInfiniteScrolling: some View {
|
||||||
|
Section(footer: Text("Require a button tap before loading more posts.")) {
|
||||||
|
Toggle(isOn: $preferences.disableInfiniteScrolling) {
|
||||||
|
Text("Disable Infinite Scrolling")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WellnessPrefsView_Previews: PreviewProvider {
|
struct WellnessPrefsView_Previews: PreviewProvider {
|
||||||
|
|
|
@ -91,11 +91,18 @@ class ProfileViewController: UIPageViewController {
|
||||||
addKeyCommand(MenuController.nextSubTabCommand)
|
addKeyCommand(MenuController.nextSubTabCommand)
|
||||||
|
|
||||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [weak self] in $0 == self?.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [weak self] in $0 == self?.accountID }
|
||||||
.sink { [weak self] (_) in self?.updateAccountUI() }
|
.sink { [weak self] (_) in self?.updateAccountUI() }
|
||||||
|
|
||||||
loadAccount()
|
loadAccount()
|
||||||
|
|
||||||
|
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
|
||||||
|
if let nav = navigationController {
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithDefaultBackground()
|
||||||
|
nav.navigationBar.scrollEdgeAppearance = appearance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -245,34 +252,6 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||||
view.pagesSegmentedControl.isUserInteractionEnabled = true
|
view.pagesSegmentedControl.isUserInteractionEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileHeader(_ view: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView) {
|
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID)!
|
|
||||||
|
|
||||||
func showActivityController(activities: [UIActivity]) {
|
|
||||||
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities)
|
|
||||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: account.url)
|
|
||||||
activityController.popoverPresentationController?.sourceView = sourceView
|
|
||||||
self.present(activityController, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account.id == mastodonController.account.id {
|
|
||||||
showActivityController(activities: [OpenInSafariActivity()])
|
|
||||||
} else {
|
|
||||||
let request = Client.getRelationships(accounts: [account.id])
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
var customActivities: [UIActivity] = [OpenInSafariActivity()]
|
|
||||||
if case let .success(results, _) = response, let relationship = results.first {
|
|
||||||
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
|
|
||||||
customActivities.insert(toggleFollowActivity, at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
showActivityController(activities: customActivities)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController: TabBarScrollableViewController {
|
extension ProfileViewController: TabBarScrollableViewController {
|
||||||
|
|
|
@ -14,7 +14,7 @@ fileprivate let accountCell = "accountCell"
|
||||||
fileprivate let statusCell = "statusCell"
|
fileprivate let statusCell = "statusCell"
|
||||||
fileprivate let hashtagCell = "hashtagCell"
|
fileprivate let hashtagCell = "hashtagCell"
|
||||||
|
|
||||||
protocol SearchResultsViewControllerDelegate: class {
|
protocol SearchResultsViewControllerDelegate: AnyObject {
|
||||||
func selectedSearchResult(account accountID: String)
|
func selectedSearchResult(account accountID: String)
|
||||||
func selectedSearchResult(hashtag: Hashtag)
|
func selectedSearchResult(hashtag: Hashtag)
|
||||||
func selectedSearchResult(status statusID: String)
|
func selectedSearchResult(status statusID: String)
|
||||||
|
@ -36,6 +36,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var activityIndicator: UIActivityIndicatorView!
|
private var activityIndicator: UIActivityIndicatorView!
|
||||||
|
private var errorLabel: UILabel!
|
||||||
|
|
||||||
/// Types of results to search for. `nil` means all results will be included.
|
/// Types of results to search for. `nil` means all results will be included.
|
||||||
var resultTypes: [SearchResultType]? = nil
|
var resultTypes: [SearchResultType]? = nil
|
||||||
|
@ -60,6 +61,22 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
errorLabel = UILabel()
|
||||||
|
errorLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
errorLabel.font = .preferredFont(forTextStyle: .callout)
|
||||||
|
errorLabel.textColor = .secondaryLabel
|
||||||
|
errorLabel.numberOfLines = 0
|
||||||
|
errorLabel.textAlignment = .center
|
||||||
|
errorLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
tableView.addSubview(errorLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
errorLabel.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||||
|
errorLabel.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
|
||||||
|
errorLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: tableView.leadingAnchor, multiplier: 1),
|
||||||
|
tableView.trailingAnchor.constraint(equalToSystemSpacingAfter: errorLabel.trailingAnchor, multiplier: 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
|
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
||||||
|
@ -131,59 +148,79 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
activityIndicator.isHidden = false
|
activityIndicator.isHidden = false
|
||||||
activityIndicator.startAnimating()
|
activityIndicator.startAnimating()
|
||||||
|
errorLabel.isHidden = true
|
||||||
|
|
||||||
let resultTypes = self.resultTypes
|
|
||||||
let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10)
|
let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(results, _) = response else { fatalError() }
|
switch response {
|
||||||
|
case let .success(results, _):
|
||||||
DispatchQueue.main.async {
|
guard self.currentQuery == query else { return }
|
||||||
self.activityIndicator.isHidden = true
|
|
||||||
self.activityIndicator.stopAnimating()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard self.currentQuery == query else { return }
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
DispatchQueue.main.async {
|
||||||
self.dataSource.apply(snapshot)
|
self.activityIndicator.isHidden = true
|
||||||
|
self.activityIndicator.stopAnimating()
|
||||||
}
|
}
|
||||||
})
|
self.showSearchResults(results)
|
||||||
|
case let .failure(error):
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showSearchError(_ error: Client.Error) {
|
||||||
|
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
|
||||||
|
errorLabel.isHidden = false
|
||||||
|
errorLabel.text = error.localizedDescription
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
|
|
@ -37,9 +37,10 @@ class SearchViewController: UIViewController {
|
||||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.exploreNavigationController = self.navigationController
|
resultsController.exploreNavigationController = self.navigationController
|
||||||
searchController = UISearchController(searchResultsController: resultsController)
|
searchController = UISearchController(searchResultsController: resultsController)
|
||||||
searchController.searchResultsUpdater = resultsController
|
searchController.obscuresBackgroundDuringPresentation = false
|
||||||
searchController.searchBar.autocapitalizationType = .none
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
searchController.searchBar.delegate = resultsController
|
searchController.searchBar.delegate = resultsController
|
||||||
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol InstanceTimelineViewControllerDelegate: class {
|
protocol InstanceTimelineViewControllerDelegate: AnyObject {
|
||||||
func didSaveInstance(url: URL)
|
func didSaveInstance(url: URL)
|
||||||
func didUnsaveInstance(url: URL)
|
func didUnsaveInstance(url: URL)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
typealias TimelineEntry = (id: String, state: StatusState)
|
typealias TimelineEntry = (id: String, state: StatusState)
|
||||||
|
|
||||||
class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry> {
|
class TimelineTableViewController: DiffableTimelineLikeTableViewController<TimelineTableViewController.Section, TimelineTableViewController.Item> {
|
||||||
|
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
@ -19,6 +19,8 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
|
private var didConfirmLoadMore = false
|
||||||
|
|
||||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
@ -40,14 +42,14 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
guard let persistentContainer = mastodonController?.persistentContainer else { return }
|
guard let persistentContainer = mastodonController?.persistentContainer,
|
||||||
|
let dataSource = dataSource else { return }
|
||||||
// decrement reference counts of any statuses we still have
|
// decrement reference counts of any statuses we still have
|
||||||
// if the app is currently being quit, this will not affect the persisted data because
|
// if the app is currently being quit, this will not affect the persisted data because
|
||||||
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
|
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
|
||||||
for section in sections {
|
// todo: remove the whole reference count system
|
||||||
for (id, _) in section {
|
for case let .status(id: id, state: _) in dataSource.snapshot().itemIdentifiers {
|
||||||
persistentContainer.status(for: id)?.decrementReferenceCount()
|
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,100 +57,157 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
||||||
|
tableView.register(UINib(nibName: "ConfirmLoadMoreTableViewCell", bundle: .main), forCellReuseIdentifier: "confirmLoadMoreCell")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - DiffableTimelineLikeTableViewController
|
||||||
|
|
||||||
override class func refreshCommandTitle() -> String {
|
override class func refreshCommandTitle() -> String {
|
||||||
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
|
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func willRemoveRows(at indexPaths: [IndexPath]) {
|
override func timelineContentSections() -> [Section] {
|
||||||
for indexPath in indexPaths {
|
return [.statuses]
|
||||||
let id = item(for: indexPath).id
|
}
|
||||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
|
||||||
|
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
|
||||||
|
switch item {
|
||||||
|
case let .status(id: id, state: state):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
||||||
|
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
return cell
|
||||||
|
|
||||||
|
case .confirmLoadMore:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "confirmLoadMoreCell", for: indexPath) as! ConfirmLoadMoreTableViewCell
|
||||||
|
cell.confirmLoadMore = {
|
||||||
|
self.didConfirmLoadMore = true
|
||||||
|
self.loadOlder()
|
||||||
|
self.didConfirmLoadMore = false
|
||||||
|
}
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) {
|
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
|
guard let mastodonController = mastodonController else {
|
||||||
|
completion(.failure(.noClient))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline)
|
let request = Client.getStatuses(timeline: timeline)
|
||||||
|
|
||||||
mastodonController?.run(request) { (response) in
|
mastodonController.run(request) { response in
|
||||||
guard case let .success(statuses, pagination) = response else {
|
switch response {
|
||||||
completion([])
|
case let .failure(error):
|
||||||
return
|
completion(.failure(.client(error)))
|
||||||
}
|
|
||||||
|
case let .success(statuses, pagination):
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
var snapshot = Snapshot()
|
||||||
|
snapshot.appendSections([.statuses, .footer])
|
||||||
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) {
|
override func loadOlderItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard let older = older else {
|
guard let older = older else {
|
||||||
completion([])
|
completion(.failure(.noOlder))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !Preferences.shared.disableInfiniteScrolling || didConfirmLoadMore else {
|
||||||
|
guard !currentSnapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
|
||||||
|
// todo: need something more accurate than "success"/"failure"
|
||||||
|
completion(.success(currentSnapshot))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var snapshot = currentSnapshot
|
||||||
|
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||||
|
self.dataSource.apply(snapshot)
|
||||||
|
completion(.success(snapshot))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||||
|
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { response in
|
||||||
guard case let .success(statuses, pagination) = response else {
|
switch response {
|
||||||
completion([])
|
case let .failure(error):
|
||||||
return
|
completion(.failure(.client(error)))
|
||||||
}
|
|
||||||
|
case let .success(statuses, pagination):
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
var snapshot = currentSnapshot
|
||||||
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
|
snapshot.deleteItems([.confirmLoadMore])
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) {
|
override func loadNewerItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard let newer = newer else {
|
guard let newer = newer else {
|
||||||
completion([])
|
completion(.failure(.noNewer))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline, range: newer)
|
let request = Client.getStatuses(timeline: timeline, range: newer)
|
||||||
|
mastodonController.run(request) { response in
|
||||||
mastodonController.run(request) { (response) in
|
switch response {
|
||||||
guard case let .success(statuses, pagination) = response else {
|
case let .failure(error):
|
||||||
completion([])
|
completion(.failure(.client(error)))
|
||||||
return
|
|
||||||
}
|
case let .success(statuses, pagination):
|
||||||
|
// if there are no new statuses, pagination is nil
|
||||||
// if there are no new statuses, pagination is nil
|
// if we were to then overwrite self.newer, future refresh would fail
|
||||||
// if we were to then overwrite self.newer, future refreshes would fail
|
if let newer = pagination?.newer {
|
||||||
if let newer = pagination?.newer {
|
self.newer = newer
|
||||||
self.newer = newer
|
}
|
||||||
}
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
var snapshot = currentSnapshot
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
let identifiers = statuses.map { Item.status(id: $0.id, state: .unknown) }
|
||||||
|
if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
|
||||||
|
snapshot.insertItems(identifiers, beforeItem: first)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems(identifiers, toSection: .statuses)
|
||||||
|
}
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
override func willRemoveItems(_ items: [Item]) {
|
||||||
|
for case let .status(id: id, state: _) in items {
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
}
|
||||||
|
|
||||||
let (id, state) = item(for: indexPath)
|
|
||||||
cell.delegate = self
|
|
||||||
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
|
|
||||||
return cell
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimelineTableViewController {
|
||||||
|
enum Section: Hashable, CaseIterable {
|
||||||
|
case statuses
|
||||||
|
case footer
|
||||||
|
}
|
||||||
|
enum Item: Hashable {
|
||||||
|
case status(id: String, state: StatusState)
|
||||||
|
case confirmLoadMore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController: TuskerNavigationDelegate {
|
extension TimelineTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
@ -161,17 +220,23 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
|
||||||
|
|
||||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
let ids = indexPaths.map { item(for: $0).id }
|
|
||||||
prefetchStatuses(with: ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
|
||||||
let ids: [String] = indexPaths.compactMap {
|
let ids: [String] = indexPaths.compactMap {
|
||||||
guard $0.section < sections.count,
|
if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
|
||||||
$0.row < sections[$0.section].count else {
|
return id
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefetchStatuses(with: ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
|
let ids: [String] = indexPaths.compactMap {
|
||||||
|
if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
|
||||||
|
return id
|
||||||
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return item(for: $0).id
|
|
||||||
}
|
}
|
||||||
cancelPrefetchingStatuses(with: ids)
|
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 SafariServices
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
protocol MenuPreviewProvider: class {
|
protocol MenuPreviewProvider: AnyObject {
|
||||||
|
|
||||||
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
|
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
|
||||||
|
|
||||||
|
@ -37,10 +37,11 @@ extension MenuPreviewProvider {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
||||||
|
|
||||||
guard mastodonController.loggedIn else {
|
guard let loggedInAccountID = mastodonController.accountInfo?.id else {
|
||||||
|
// user is logged out
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: account.url),
|
openInSafariAction(url: account.url),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -63,38 +64,43 @@ extension MenuPreviewProvider {
|
||||||
let request = Client.getRelationships(accounts: [account.id])
|
let request = Client.getRelationships(accounts: [account.id])
|
||||||
// talk about callback hell :/
|
// talk about callback hell :/
|
||||||
mastodonController.run(request) { [weak self] (response) in
|
mastodonController.run(request) { [weak self] (response) in
|
||||||
if let self = self,
|
guard let self = self,
|
||||||
case let .success(results, _) = response,
|
case let .success(results, _) = response,
|
||||||
let relationship = results.first {
|
let relationship = results.first else {
|
||||||
let following = relationship.following
|
elementHandler([])
|
||||||
DispatchQueue.main.async {
|
return
|
||||||
elementHandler([
|
}
|
||||||
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
|
let following = relationship.following
|
||||||
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
DispatchQueue.main.async {
|
||||||
mastodonController.run(request) { (response) in
|
let action = self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
|
||||||
switch response {
|
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
||||||
case .failure(_):
|
mastodonController.run(request) { (response) in
|
||||||
fatalError()
|
switch response {
|
||||||
case let .success(relationship, _):
|
case .failure(_):
|
||||||
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
fatalError()
|
||||||
}
|
case let .success(relationship, _):
|
||||||
}
|
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||||
})
|
}
|
||||||
])
|
}
|
||||||
}
|
})
|
||||||
|
elementHandler([
|
||||||
|
action
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
let shareSection = [
|
var shareSection = [
|
||||||
openInSafariAction(url: account.url),
|
openInSafariAction(url: account.url),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
||||||
|
@ -104,7 +110,7 @@ extension MenuPreviewProvider {
|
||||||
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: url),
|
openInSafariAction(url: url),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -133,13 +139,14 @@ extension MenuPreviewProvider {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForStatus(_ status: StatusMO, sourceView: UIView?) -> [UIMenuElement] {
|
func actionsForStatus(_ status: StatusMO, sourceView: UIView?, includeReply: Bool = true) -> [UIMenuElement] {
|
||||||
guard let mastodonController = mastodonController else { return [] }
|
guard let mastodonController = mastodonController else { return [] }
|
||||||
|
|
||||||
guard mastodonController.loggedIn else {
|
guard let accountID = mastodonController.accountInfo?.id else {
|
||||||
|
// user is logged out
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: status.url!),
|
openInSafariAction(url: status.url!),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -150,10 +157,6 @@ extension MenuPreviewProvider {
|
||||||
let muted = status.muted
|
let muted = status.muted
|
||||||
|
|
||||||
var actionsSection = [
|
var actionsSection = [
|
||||||
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.navigationDelegate?.compose(inReplyToID: status.id)
|
|
||||||
}),
|
|
||||||
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
|
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
|
||||||
|
@ -174,9 +177,16 @@ extension MenuPreviewProvider {
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if includeReply {
|
||||||
|
actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.navigationDelegate?.compose(inReplyToID: status.id)
|
||||||
|
}), at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
||||||
let pinned = status.pinned ?? false
|
let pinned = status.pinned ?? false
|
||||||
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin" : "Pin", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
||||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||||
|
@ -204,21 +214,13 @@ extension MenuPreviewProvider {
|
||||||
|
|
||||||
var shareSection = [
|
var shareSection = [
|
||||||
openInSafariAction(url: status.url!),
|
openInSafariAction(url: status.url!),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
#if targetEnvironment(macCatalyst)
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
||||||
shareSection.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "", handler: { (_) in
|
|
||||||
guard let id = mastodonController.accountInfo?.id else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// todo: this should try to find an existing session
|
|
||||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: id), options: nil, errorHandler: nil)
|
|
||||||
}))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||||
|
@ -242,6 +244,28 @@ extension MenuPreviewProvider {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
|
||||||
|
#if SDK_IOS_15
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
|
options.preferredPresentationStyle = .automatic
|
||||||
|
actions.append(UIWindowScene.ActivationAction { (_) in
|
||||||
|
return .init(userActivity: activity(), options: options, preview: nil)
|
||||||
|
})
|
||||||
|
} else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LargeImageViewController: CustomPreviewPresenting {
|
extension LargeImageViewController: CustomPreviewPresenting {
|
||||||
|
|
|
@ -44,6 +44,13 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
||||||
|
|
||||||
addKeyCommand(MenuController.prevSubTabCommand)
|
addKeyCommand(MenuController.prevSubTabCommand)
|
||||||
addKeyCommand(MenuController.nextSubTabCommand)
|
addKeyCommand(MenuController.nextSubTabCommand)
|
||||||
|
|
||||||
|
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
|
||||||
|
if let nav = navigationController {
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithDefaultBackground()
|
||||||
|
nav.navigationBar.scrollEdgeAppearance = appearance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectPage(at index: Int, animated: Bool) {
|
func selectPage(at index: Int, animated: Bool) {
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// A table view controller that manages common functionality between timeline-like UIs.
|
/// A table view controller that manages common functionality between timeline-like UIs.
|
||||||
// For example, this class handles loading new items when the user scrolls to the end,
|
/// For example, this class handles loading new items when the user scrolls to the end,
|
||||||
// refreshing, and pruning offscreen rows automatically.
|
/// refreshing, and pruning offscreen rows automatically.
|
||||||
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
|
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
|
||||||
|
|
||||||
private(set) var loaded = false
|
private(set) var loaded = false
|
||||||
|
|
|
@ -37,6 +37,15 @@ class UserActivityManager {
|
||||||
}
|
}
|
||||||
return LocalData.shared.getAccount(id: id)
|
return LocalData.shared.getAccount(id: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Main Scene
|
||||||
|
static func mainSceneActivity(accountID: String) -> NSUserActivity {
|
||||||
|
let activity = NSUserActivity(type: .mainScene)
|
||||||
|
activity.userInfo = [
|
||||||
|
"accountID": accountID,
|
||||||
|
]
|
||||||
|
return activity
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - New Post
|
// MARK: - New Post
|
||||||
static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity {
|
static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity {
|
||||||
|
@ -61,6 +70,7 @@ class UserActivityManager {
|
||||||
// TODO: check not currently showing compose screen
|
// TODO: check not currently showing compose screen
|
||||||
let mentioning = activity.userInfo?["mentioning"] as? String
|
let mentioning = activity.userInfo?["mentioning"] as? String
|
||||||
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
||||||
|
// todo: this shouldn't use self.mastodonController, it should get the right one based on the userInfo accountID
|
||||||
let composeVC = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
let composeVC = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
||||||
present(UINavigationController(rootViewController: composeVC))
|
present(UINavigationController(rootViewController: composeVC))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum UserActivityType: String {
|
enum UserActivityType: String {
|
||||||
|
case mainScene = "space.vaccor.Tusker.activity.main-scene"
|
||||||
case newPost = "space.vaccor.Tusker.activity.new-post"
|
case newPost = "space.vaccor.Tusker.activity.new-post"
|
||||||
case checkNotifications = "space.vaccor.Tusker.activity.check-notifications"
|
case checkNotifications = "space.vaccor.Tusker.activity.check-notifications"
|
||||||
case showTimeline = "space.vaccor.Tusker.activity.show-timeline"
|
case showTimeline = "space.vaccor.Tusker.activity.show-timeline"
|
||||||
|
@ -22,6 +23,8 @@ enum UserActivityType: String {
|
||||||
extension UserActivityType {
|
extension UserActivityType {
|
||||||
var handle: (NSUserActivity) -> Void {
|
var handle: (NSUserActivity) -> Void {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .mainScene:
|
||||||
|
fatalError("cannot handle main scene activity")
|
||||||
case .newPost:
|
case .newPost:
|
||||||
return UserActivityManager.handleNewPost
|
return UserActivityManager.handleNewPost
|
||||||
case .checkNotifications:
|
case .checkNotifications:
|
||||||
|
|
|
@ -89,15 +89,22 @@ extension TuskerNavigationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft) {
|
func compose(editing draft: Draft) {
|
||||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
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 vc = UINavigationController(rootViewController: compose)
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
vc.presentationController?.delegate = compose
|
options.preferredPresentationStyle = .prominent
|
||||||
present(vc, animated: true)
|
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) {
|
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
||||||
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
||||||
|
DraftsManager.shared.add(draft)
|
||||||
compose(editing: draft)
|
compose(editing: draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
import Gifu
|
import Gifu
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
protocol AttachmentViewDelegate: class {
|
protocol AttachmentViewDelegate: AnyObject {
|
||||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
|
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
|
||||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,9 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil)
|
||||||
|
|
||||||
addInteraction(UIContextMenuInteraction(delegate: self))
|
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
|
|
||||||
|
isAccessibilityElement = true
|
||||||
|
accessibilityTraits = [.image, .button]
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
|
@ -161,7 +164,9 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
let attachmentURL = attachment.url
|
let attachmentURL = attachment.url
|
||||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
|
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
|
||||||
guard let self = self, let data = data else { return }
|
guard let self = self, let data = data else { return }
|
||||||
self.attachmentRequest = nil
|
DispatchQueue.main.async {
|
||||||
|
self.attachmentRequest = nil
|
||||||
|
}
|
||||||
if self.attachment.url.pathExtension == "gif" {
|
if self.attachment.url.pathExtension == "gif" {
|
||||||
self.source = .gifData(attachmentURL, data)
|
self.source = .gifData(attachmentURL, data)
|
||||||
if self.autoplayGifs {
|
if self.autoplayGifs {
|
||||||
|
@ -293,6 +298,13 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
@objc func imagePressed() {
|
@objc func imagePressed() {
|
||||||
showGallery()
|
showGallery()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Accessibility
|
||||||
|
|
||||||
|
override func accessibilityActivate() -> Bool {
|
||||||
|
showGallery()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -248,9 +248,11 @@ class AttachmentsContainerView: UIView {
|
||||||
let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size)
|
let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size)
|
||||||
attachmentView.delegate = delegate
|
attachmentView.delegate = delegate
|
||||||
attachmentView.translatesAutoresizingMaskIntoConstraints = false
|
attachmentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
attachmentView.isAccessibilityElement = true
|
|
||||||
attachmentView.accessibilityTraits = [.image, .button]
|
|
||||||
attachmentView.accessibilityLabel = String(format: NSLocalizedString("Attachment %d", comment: "attachment at index accessiblity label"), index + 1)
|
attachmentView.accessibilityLabel = String(format: NSLocalizedString("Attachment %d", comment: "attachment at index accessiblity label"), index + 1)
|
||||||
|
attachmentView.accessibilityLabel = "Attachment \(index + 1)"
|
||||||
|
if let desc = attachments[index].description {
|
||||||
|
attachmentView.accessibilityLabel! += ", \(desc)"
|
||||||
|
}
|
||||||
attachmentViews.add(attachmentView)
|
attachmentViews.add(attachmentView)
|
||||||
return attachmentView
|
return attachmentView
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
|
||||||
protocol BaseEmojiLabel: class {
|
protocol BaseEmojiLabel: AnyObject {
|
||||||
var emojiIdentifier: String? { get set }
|
var emojiIdentifier: String? { get set }
|
||||||
var emojiRequests: [ImageCache.Request] { get set }
|
var emojiRequests: [ImageCache.Request] { get set }
|
||||||
var emojiFont: UIFont { get }
|
var emojiFont: UIFont { get }
|
||||||
|
|
|
@ -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()
|
let label = EmojiLabel()
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
label.numberOfLines = 0
|
label.numberOfLines = 0
|
||||||
|
label.font = .preferredFont(forTextStyle: .callout)
|
||||||
label.text = option.title
|
label.text = option.title
|
||||||
label.setEmojis(poll.emojis, identifier: poll.id)
|
label.setEmojis(poll.emojis, identifier: poll.id)
|
||||||
addSubview(label)
|
addSubview(label)
|
||||||
|
@ -37,15 +38,21 @@ class PollOptionView: UIView {
|
||||||
let percentLabel = UILabel()
|
let percentLabel = UILabel()
|
||||||
percentLabel.translatesAutoresizingMaskIntoConstraints = false
|
percentLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
percentLabel.text = "100%"
|
percentLabel.text = "100%"
|
||||||
|
percentLabel.font = label.font
|
||||||
percentLabel.isHidden = true
|
percentLabel.isHidden = true
|
||||||
|
percentLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
percentLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
addSubview(percentLabel)
|
addSubview(percentLabel)
|
||||||
|
|
||||||
|
accessibilityLabel = option.title
|
||||||
|
|
||||||
if (poll.voted ?? false) || poll.effectiveExpired,
|
if (poll.voted ?? false) || poll.effectiveExpired,
|
||||||
let optionVotes = option.votesCount {
|
let optionVotes = option.votesCount {
|
||||||
let frac = poll.votesCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(poll.votesCount)
|
let frac = poll.votesCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(poll.votesCount)
|
||||||
|
let percent = String(format: "%.0f%%", frac * 100)
|
||||||
|
|
||||||
percentLabel.isHidden = false
|
percentLabel.isHidden = false
|
||||||
percentLabel.text = String(format: "%.0f%%", frac * 100)
|
percentLabel.text = percent
|
||||||
|
|
||||||
let fillView = UIView()
|
let fillView = UIView()
|
||||||
fillView.translatesAutoresizingMaskIntoConstraints = false
|
fillView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -60,6 +67,8 @@ class PollOptionView: UIView {
|
||||||
fillView.topAnchor.constraint(equalTo: topAnchor),
|
fillView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
fillView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
fillView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
accessibilityLabel! += ", \(percent)"
|
||||||
}
|
}
|
||||||
|
|
||||||
let minHeightConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
|
let minHeightConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
|
||||||
|
@ -76,12 +85,14 @@ class PollOptionView: UIView {
|
||||||
label.topAnchor.constraint(equalTo: topAnchor),
|
label.topAnchor.constraint(equalTo: topAnchor),
|
||||||
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
|
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
|
||||||
label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor),
|
label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4),
|
||||||
|
|
||||||
percentLabel.topAnchor.constraint(equalTo: topAnchor),
|
percentLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||||
percentLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
percentLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
isAccessibilityElement = true
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
|
|
@ -69,6 +69,8 @@ class PollOptionsView: UIControl {
|
||||||
stack.addArrangedSubview(optionView)
|
stack.addArrangedSubview(optionView)
|
||||||
return optionView
|
return optionView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accessibilityElements = options
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectOption(_ option: PollOptionView) {
|
private func selectOption(_ option: PollOptionView) {
|
||||||
|
|
|
@ -23,17 +23,20 @@ class StatusPollView: UIView {
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var statusID: String!
|
private var statusID: String!
|
||||||
private var poll: Poll!
|
private(set) var poll: Poll?
|
||||||
|
|
||||||
private var optionsView: PollOptionsView!
|
private var optionsView: PollOptionsView!
|
||||||
private var voteButton: UIButton!
|
private var voteButton: UIButton!
|
||||||
private var infoLabel: UILabel!
|
private var infoLabel: UILabel!
|
||||||
private var options: [PollOptionView] = []
|
|
||||||
|
|
||||||
private var canVote = true
|
private var canVote = true
|
||||||
private var animator: UIViewPropertyAnimator!
|
private var animator: UIViewPropertyAnimator!
|
||||||
private var currentSelectedOptionIndex: Int!
|
private var currentSelectedOptionIndex: Int!
|
||||||
|
|
||||||
|
var isTracking: Bool {
|
||||||
|
optionsView.isTracking
|
||||||
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
@ -74,13 +77,15 @@ class StatusPollView: UIView {
|
||||||
voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
||||||
voteButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
voteButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
accessibilityElements = [optionsView!, infoLabel!, voteButton!]
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(status: StatusMO, poll: Poll) {
|
func updateUI(status: StatusMO, poll: Poll?) {
|
||||||
self.statusID = status.id
|
self.statusID = status.id
|
||||||
self.poll = poll
|
self.poll = poll
|
||||||
|
|
||||||
options.forEach { $0.removeFromSuperview() }
|
guard let poll = poll else { return }
|
||||||
|
|
||||||
// poll.voted is nil if there is no user (e.g., public timeline), in which case the poll also cannot be voted upon
|
// poll.voted is nil if there is no user (e.g., public timeline), in which case the poll also cannot be voted upon
|
||||||
if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account.id {
|
if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account.id {
|
||||||
|
@ -138,7 +143,7 @@ class StatusPollView: UIView {
|
||||||
|
|
||||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
|
|
||||||
let request = Poll.vote(poll.id, choices: optionsView.checkedOptionIndices)
|
let request = Poll.vote(poll!.id, choices: optionsView.checkedOptionIndices)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
|
|
|
@ -12,8 +12,6 @@ import Combine
|
||||||
|
|
||||||
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate {
|
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate {
|
||||||
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int)
|
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int)
|
||||||
|
|
||||||
func profileHeader(_ headerView: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfileHeaderView: UIView {
|
class ProfileHeaderView: UIView {
|
||||||
|
@ -82,14 +80,14 @@ class ProfileHeaderView: UIView {
|
||||||
cancellables = []
|
cancellables = []
|
||||||
|
|
||||||
mastodonController.persistentContainer.accountSubject
|
mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [weak self] in $0 == self?.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [weak self] in $0 == self?.accountID }
|
||||||
.sink { [weak self] in self?.updateUI(for: $0) }
|
.sink { [weak self] in self?.updateUI(for: $0) }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
mastodonController.persistentContainer.relationshipSubject
|
mastodonController.persistentContainer.relationshipSubject
|
||||||
.filter { [weak self] in $0 == self?.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [weak self] in $0 == self?.accountID }
|
||||||
.sink { [weak self] (_) in self?.updateRelationship() }
|
.sink { [weak self] (_) in self?.updateRelationship() }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
@ -134,6 +132,7 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
|
var fieldAccessibilityElements = [Any]()
|
||||||
for field in account.fields {
|
for field in account.fields {
|
||||||
let nameLabel = EmojiLabel()
|
let nameLabel = EmojiLabel()
|
||||||
nameLabel.text = field.name
|
nameLabel.text = field.name
|
||||||
|
@ -157,7 +156,18 @@ class ProfileHeaderView: UIView {
|
||||||
fieldValuesStackView.addArrangedSubview(valueTextView)
|
fieldValuesStackView.addArrangedSubview(valueTextView)
|
||||||
|
|
||||||
nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true
|
nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true
|
||||||
|
fieldAccessibilityElements.append(nameLabel)
|
||||||
|
fieldAccessibilityElements.append(valueTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accessibilityElements = [
|
||||||
|
displayNameLabel!,
|
||||||
|
usernameLabel!,
|
||||||
|
noteTextView!,
|
||||||
|
] + fieldAccessibilityElements + [
|
||||||
|
moreButton!,
|
||||||
|
pagesSegmentedControl!,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRelationship() {
|
private func updateRelationship() {
|
||||||
|
@ -193,11 +203,14 @@ class ProfileHeaderView: UIView {
|
||||||
let image = image,
|
let image = image,
|
||||||
self.accountID == accountID,
|
self.accountID == accountID,
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.avatarRequest = nil
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.avatarRequest = nil
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.avatarRequest = nil
|
||||||
self.avatarImageView.image = transformedImage
|
self.avatarImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,11 +220,14 @@ class ProfileHeaderView: UIView {
|
||||||
let image = image,
|
let image = image,
|
||||||
self.accountID == accountID,
|
self.accountID == accountID,
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.headerRequest = nil
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.headerRequest = nil
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.headerRequest = nil
|
||||||
self.headerImageView.image = transformedImage
|
self.headerImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,8 +94,7 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
collapseButton.layer.masksToBounds = true
|
collapseButton.layer.masksToBounds = true
|
||||||
collapseButton.layer.cornerRadius = 5
|
collapseButton.layer.cornerRadius = 5
|
||||||
|
|
||||||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!, pollView!]
|
||||||
attachmentsView.isAccessibilityElement = true
|
|
||||||
|
|
||||||
moreButton.showsMenuAsPrimaryAction = true
|
moreButton.showsMenuAsPrimaryAction = true
|
||||||
|
|
||||||
|
@ -107,8 +106,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
open func createObserversIfNecessary() {
|
open func createObserversIfNecessary() {
|
||||||
if statusUpdater == nil {
|
if statusUpdater == nil {
|
||||||
statusUpdater = mastodonController.persistentContainer.statusSubject
|
statusUpdater = mastodonController.persistentContainer.statusSubject
|
||||||
.filter { [unowned self] in $0 == self.statusID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [unowned self] in $0 == self.statusID }
|
||||||
.sink { [unowned self] in
|
.sink { [unowned self] in
|
||||||
if let mastodonController = mastodonController,
|
if let mastodonController = mastodonController,
|
||||||
let status = mastodonController.persistentContainer.status(for: $0) {
|
let status = mastodonController.persistentContainer.status(for: $0) {
|
||||||
|
@ -119,8 +118,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
|
|
||||||
if accountUpdater == nil {
|
if accountUpdater == nil {
|
||||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [unowned self] in $0 == self.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [unowned self] in $0 == self.accountID }
|
||||||
.sink { [unowned self] in
|
.sink { [unowned self] in
|
||||||
if let mastodonController = mastodonController,
|
if let mastodonController = mastodonController,
|
||||||
let account = mastodonController.persistentContainer.account(for: $0) {
|
let account = mastodonController.persistentContainer.account(for: $0) {
|
||||||
|
@ -157,8 +156,6 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
cardView.navigationDelegate = navigationDelegate
|
cardView.navigationDelegate = navigationDelegate
|
||||||
|
|
||||||
attachmentsView.updateUI(status: status)
|
attachmentsView.updateUI(status: status)
|
||||||
attachmentsView.isAccessibilityElement = status.attachments.count > 0
|
|
||||||
attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count)
|
|
||||||
|
|
||||||
updateStatusState(status: status)
|
updateStatusState(status: status)
|
||||||
|
|
||||||
|
@ -213,15 +210,12 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton))
|
// do not include reply action here, because the cell already contains a button for it
|
||||||
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton, includeReply: false))
|
||||||
|
|
||||||
if let poll = status.poll {
|
pollView.isHidden = status.poll == nil
|
||||||
pollView.isHidden = false
|
pollView.mastodonController = mastodonController
|
||||||
pollView.mastodonController = mastodonController
|
pollView.updateUI(status: status, poll: status.poll)
|
||||||
pollView.updateUI(status: status, poll: poll)
|
|
||||||
} else {
|
|
||||||
pollView.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
|
@ -359,6 +353,7 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
contentTextView.isHidden = collapsed
|
contentTextView.isHidden = collapsed
|
||||||
cardView.isHidden = cardView.card == nil || collapsed
|
cardView.isHidden = cardView.card == nil || collapsed
|
||||||
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
|
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
|
||||||
|
pollView.isHidden = pollView.poll == nil || collapsed
|
||||||
|
|
||||||
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
|
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,21 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
|
|
||||||
profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
|
profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
|
||||||
profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self)
|
profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self)
|
||||||
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentTextView!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
|
accessibilityElements = [
|
||||||
|
profileAccessibilityElement!,
|
||||||
|
contentWarningLabel!,
|
||||||
|
collapseButton!,
|
||||||
|
contentTextView!,
|
||||||
|
attachmentsView!,
|
||||||
|
pollView!,
|
||||||
|
totalFavoritesButton!,
|
||||||
|
totalReblogsButton!,
|
||||||
|
timestampAndClientLabel!,
|
||||||
|
replyButton!,
|
||||||
|
favoriteButton!,
|
||||||
|
reblogButton!,
|
||||||
|
moreButton!,
|
||||||
|
]
|
||||||
|
|
||||||
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
static let relativeDateFormatter: RelativeDateTimeFormatter = {
|
static let relativeDateFormatter: RelativeDateTimeFormatter = {
|
||||||
let formatter = RelativeDateTimeFormatter()
|
let formatter = RelativeDateTimeFormatter()
|
||||||
formatter.dateTimeStyle = .numeric
|
formatter.dateTimeStyle = .numeric
|
||||||
formatter.unitsStyle = .short
|
formatter.unitsStyle = .full
|
||||||
return formatter
|
return formatter
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
|
|
||||||
reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
|
reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
|
||||||
|
|
||||||
accessibilityElements!.insert(reblogLabel!, at: 0)
|
isAccessibilityElement = true
|
||||||
|
|
||||||
// todo: double check this on RTL layouts
|
// todo: double check this on RTL layouts
|
||||||
replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
|
replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
|
||||||
|
@ -59,8 +59,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
|
|
||||||
if rebloggerAccountUpdater == nil {
|
if rebloggerAccountUpdater == nil {
|
||||||
rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject
|
rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [unowned self] in $0 == self.rebloggerID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [unowned self] in $0 == self.rebloggerID }
|
||||||
.sink { [unowned self] in
|
.sink { [unowned self] in
|
||||||
if let mastodonController = self.mastodonController,
|
if let mastodonController = self.mastodonController,
|
||||||
let reblogger = mastodonController.persistentContainer.account(for: $0) {
|
let reblogger = mastodonController.persistentContainer.account(for: $0) {
|
||||||
|
@ -134,7 +134,6 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
|
|
||||||
private func doUpdateTimestamp(status: StatusMO) {
|
private func doUpdateTimestamp(status: StatusMO) {
|
||||||
timestampLabel.text = status.createdAt.timeAgoString()
|
timestampLabel.text = status.createdAt.timeAgoString()
|
||||||
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
|
|
||||||
|
|
||||||
let delay: DispatchTimeInterval?
|
let delay: DispatchTimeInterval?
|
||||||
switch status.createdAt.timeAgo().1 {
|
switch status.createdAt.timeAgo().1 {
|
||||||
|
@ -193,6 +192,38 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
actions: { self.actionsForStatus(status, sourceView: self) }
|
actions: { self.actionsForStatus(status, sourceView: self) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,8 +326,11 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
|
|
||||||
extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
||||||
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
||||||
|
// the poll options view is tracking while the user is dragging between options
|
||||||
|
// while that's happening, don't initiate a drag
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID),
|
guard let status = mastodonController.persistentContainer.status(for: statusID),
|
||||||
let accountID = mastodonController.accountInfo?.id else {
|
let accountID = mastodonController.accountInfo?.id,
|
||||||
|
!pollView.isTracking else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: status.url! as NSURL)
|
let provider = NSItemProvider(object: status.url! as NSURL)
|
||||||
|
@ -314,4 +348,19 @@ extension TimelineStatusTableViewCell: UIContextMenuInteractionDelegate {
|
||||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView))
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
|
if let viewController = animator.previewViewController,
|
||||||
|
let delegate = navigationDelegate {
|
||||||
|
animator.preferredCommitStyle = .pop
|
||||||
|
animator.addCompletion {
|
||||||
|
if let customPresenting = viewController as? CustomPreviewPresenting {
|
||||||
|
customPresenting.presentFromPreview(presenter: delegate)
|
||||||
|
} else {
|
||||||
|
delegate.show(viewController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19115.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19107.4"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -38,7 +39,7 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</imageView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
|
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
|
||||||
<rect key="frame" x="58" y="0.0" width="277" height="169.5"/>
|
<rect key="frame" x="58" y="0.0" width="277" height="173.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
|
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/>
|
||||||
|
@ -132,13 +133,19 @@
|
||||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
</view>
|
</view>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="1" verticalCompressionResistancePriority="1" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oFl-rC-EEN">
|
||||||
|
<rect key="frame" x="0.0" y="173.5" width="277" height="0.0"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
||||||
<rect key="frame" x="0.0" y="54" width="50" height="22"/>
|
<rect key="frame" x="0.0" y="54" width="50" height="22"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
|
||||||
<rect key="frame" x="0.0" y="1" width="25.5" height="21.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="25.5" height="21.5"/>
|
||||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||||
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
|
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
@ -218,7 +225,7 @@
|
||||||
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/>
|
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/>
|
||||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
|
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
|
||||||
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="trailing" secondItem="ve3-Y1-NQH" secondAttribute="trailingMargin" id="3l0-tE-Ak1"/>
|
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="trailing" secondItem="ve3-Y1-NQH" secondAttribute="trailingMargin" id="3l0-tE-Ak1"/>
|
||||||
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="top" secondItem="gIY-Wp-RSk" secondAttribute="bottom" id="4KL-a3-qyf"/>
|
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="top" secondItem="gIY-Wp-RSk" secondAttribute="bottom" constant="-4" id="4KL-a3-qyf"/>
|
||||||
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
|
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
|
||||||
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="oie-wK-IpU" secondAttribute="bottom" id="7Xp-Sa-Rfk"/>
|
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="oie-wK-IpU" secondAttribute="bottom" id="7Xp-Sa-Rfk"/>
|
||||||
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
|
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
|
||||||
|
@ -276,7 +283,7 @@
|
||||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||||
<image name="globe" catalog="system" width="128" height="121"/>
|
<image name="globe" catalog="system" width="128" height="121"/>
|
||||||
<image name="pin.fill" catalog="system" width="119" height="128"/>
|
<image name="pin.fill" catalog="system" width="119" height="128"/>
|
||||||
<image name="repeat" catalog="system" width="128" height="99"/>
|
<image name="repeat" catalog="system" width="128" height="98"/>
|
||||||
<image name="star.fill" catalog="system" width="128" height="116"/>
|
<image name="star.fill" catalog="system" width="128" height="116"/>
|
||||||
<systemColor name="labelColor">
|
<systemColor name="labelColor">
|
||||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
|
|
@ -55,6 +55,8 @@ class VisualEffectImageButton: UIControl {
|
||||||
addInteraction(UIContextMenuInteraction(delegate: self))
|
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
|
|
||||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
|
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
|
||||||
|
|
||||||
|
isAccessibilityElement = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func onTap() {
|
@objc private func onTap() {
|
||||||
|
|
Loading…
Reference in New Issue