Compare commits
7 Commits
5e9cc430c6
...
911e66a159
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 911e66a159 | |
Shadowfacts | ab4bcfa50f | |
Shadowfacts | b94bfca406 | |
Shadowfacts | 7999ecafd0 | |
Shadowfacts | 1c6e464a4c | |
Shadowfacts | acd01a81cc | |
Shadowfacts | 8ac3deb55a |
|
@ -253,6 +253,7 @@
|
||||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */; };
|
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */; };
|
||||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */; };
|
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */; };
|
||||||
D6D4CC91250D2C3100FCCF8D /* UIAccessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */; };
|
D6D4CC91250D2C3100FCCF8D /* UIAccessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */; };
|
||||||
|
D6D4CC94250DB86A00FCCF8D /* ComposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4CC93250DB86A00FCCF8D /* ComposeTests.swift */; };
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; };
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; };
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD6212518A200E1C4BB /* Assets.xcassets */; };
|
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD6212518A200E1C4BB /* Assets.xcassets */; };
|
||||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; };
|
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; };
|
||||||
|
@ -575,6 +576,7 @@
|
||||||
D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContainerView.swift; sourceTree = "<group>"; };
|
D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContainerView.swift; sourceTree = "<group>"; };
|
||||||
D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeUIState.swift; sourceTree = "<group>"; };
|
D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeUIState.swift; sourceTree = "<group>"; };
|
||||||
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAccessibility.swift; sourceTree = "<group>"; };
|
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAccessibility.swift; sourceTree = "<group>"; };
|
||||||
|
D6D4CC93250DB86A00FCCF8D /* ComposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTests.swift; sourceTree = "<group>"; };
|
||||||
D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
@ -1387,6 +1389,7 @@
|
||||||
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */,
|
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */,
|
||||||
D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */,
|
D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */,
|
||||||
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */,
|
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */,
|
||||||
|
D6D4CC93250DB86A00FCCF8D /* ComposeTests.swift */,
|
||||||
D6A5BB2623BAC88E003BF21D /* Preferences */,
|
D6A5BB2623BAC88E003BF21D /* Preferences */,
|
||||||
D6D4DDF1212518A200E1C4BB /* Info.plist */,
|
D6D4DDF1212518A200E1C4BB /* Info.plist */,
|
||||||
);
|
);
|
||||||
|
@ -1950,6 +1953,7 @@
|
||||||
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */,
|
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */,
|
||||||
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */,
|
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */,
|
||||||
D6A5BB2B23BAEF61003BF21D /* APIMocks.swift in Sources */,
|
D6A5BB2B23BAEF61003BF21D /* APIMocks.swift in Sources */,
|
||||||
|
D6D4CC94250DB86A00FCCF8D /* ComposeTests.swift in Sources */,
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */,
|
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -2147,7 +2151,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
@ -2203,7 +2207,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|
|
@ -27,15 +27,6 @@
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D6D4DDCB212518A000E1C4BB"
|
|
||||||
BuildableName = "Tusker.app"
|
|
||||||
BlueprintName = "Tusker"
|
|
||||||
ReferencedContainer = "container:Tusker.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO">
|
skipped = "NO">
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
"repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git",
|
"repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"revision": "ab77ad89b449b8c415441a6ac97e0922d6900dcc",
|
"revision": "aa0f5192eaf19d01c89dbfa9ec5878a700376f23",
|
||||||
"version": null
|
"version": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -36,10 +36,10 @@ class OpenInSafariActivity: UIActivity {
|
||||||
activityDidFinish(true)
|
activityDidFinish(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func completionHandler(viewController: UIViewController, url: URL) -> UIActivityViewController.CompletionWithItemsHandler {
|
static func completionHandler(navigator: TuskerNavigationDelegate, url: URL) -> UIActivityViewController.CompletionWithItemsHandler {
|
||||||
return { (activityType, _, _, _) in
|
return { (activityType, _, _, _) in
|
||||||
if activityType == .openInSafari {
|
if activityType == .openInSafari {
|
||||||
viewController.present(SFSafariViewController(url: url), animated: true)
|
navigator.show(SFSafariViewController(url: url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,10 @@ class MastodonController {
|
||||||
var account: Account!
|
var account: Account!
|
||||||
var instance: Instance!
|
var instance: Instance!
|
||||||
|
|
||||||
|
var loggedIn: Bool {
|
||||||
|
accountInfo != nil
|
||||||
|
}
|
||||||
|
|
||||||
init(instanceURL: URL, transient: Bool = false) {
|
init(instanceURL: URL, transient: Bool = false) {
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.accountInfo = nil
|
self.accountInfo = nil
|
||||||
|
|
|
@ -18,6 +18,7 @@ class LocalData: ObservableObject {
|
||||||
private init() {
|
private init() {
|
||||||
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING") {
|
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING") {
|
||||||
defaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).uitesting")!
|
defaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).uitesting")!
|
||||||
|
defaults.removePersistentDomain(forName: "\(Bundle.main.bundleIdentifier!).uitesting")
|
||||||
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") {
|
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") {
|
||||||
accounts = [
|
accounts = [
|
||||||
UserAccountInfo(
|
UserAccountInfo(
|
||||||
|
|
|
@ -19,6 +19,8 @@ struct ComposeCurrentAccount: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
ComposeAvatarImageView(url: account.avatar)
|
ComposeAvatarImageView(url: account.avatar)
|
||||||
|
.accessibility(label: Text("\(account.displayName) avatar"))
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
AccountDisplayNameLabel(account: mastodonController.persistentContainer.account(for: account.id)!, fontSize: 20)
|
AccountDisplayNameLabel(account: mastodonController.persistentContainer.account(for: account.id)!, fontSize: 20)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
|
@ -85,8 +85,22 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
// can't do this in viewDidLoad because viewDidLoad isn't called for UIHostingController
|
// can't do this in viewDidLoad because viewDidLoad isn't called for UIHostingController
|
||||||
if mainToolbar.superview == nil {
|
// if mainToolbar.superview == nil {
|
||||||
view.addSubview(mainToolbar)
|
// view.addSubview(mainToolbar)
|
||||||
|
// NSLayoutConstraint.activate([
|
||||||
|
// mainToolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
// mainToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
// // use the top anchor of the toolbar so our additionalSafeAreaInsets (which has the bottom as the toolbar height) don't affect it
|
||||||
|
// mainToolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||||
|
// ])
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMove(toParent parent: UIViewController?) {
|
||||||
|
super.didMove(toParent: parent)
|
||||||
|
|
||||||
|
if let parent = parent {
|
||||||
|
parent.view.addSubview(mainToolbar)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
mainToolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
mainToolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
mainToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
mainToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
@ -108,6 +122,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
private func createToolbar() -> UIToolbar {
|
private func createToolbar() -> UIToolbar {
|
||||||
let toolbar = UIToolbar()
|
let toolbar = UIToolbar()
|
||||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
toolbar.isAccessibilityElement = true
|
||||||
|
|
||||||
let visibilityAction: Selector?
|
let visibilityAction: Selector?
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
|
@ -204,6 +219,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
||||||
for item in visibilityBarButtonItems {
|
for item in visibilityBarButtonItems {
|
||||||
item.image = UIImage(systemName: newVisibility.imageName)
|
item.image = UIImage(systemName: newVisibility.imageName)
|
||||||
|
item.image!.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||||
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||||
|
|
|
@ -117,6 +117,7 @@ struct ComposeView: View {
|
||||||
Text(verbatim: charactersRemaining.description)
|
Text(verbatim: charactersRemaining.description)
|
||||||
.foregroundColor(charactersRemaining < 0 ? .red : .secondary)
|
.foregroundColor(charactersRemaining < 0 ? .red : .secondary)
|
||||||
.font(Font.body.monospacedDigit())
|
.font(Font.body.monospacedDigit())
|
||||||
|
.accessibility(label: Text(charactersRemaining < 0 ? "\(-charactersRemaining) characters too many" : "\(charactersRemaining) characters remaining"))
|
||||||
}.frame(height: 50)
|
}.frame(height: 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,185 +0,0 @@
|
||||||
//
|
|
||||||
// CompositionAttachment.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 1/1/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Photos
|
|
||||||
import MobileCoreServices
|
|
||||||
|
|
||||||
enum CompositionAttachmentData {
|
|
||||||
case asset(PHAsset)
|
|
||||||
case image(UIImage)
|
|
||||||
case video(URL)
|
|
||||||
|
|
||||||
var type: AttachmentType {
|
|
||||||
switch self {
|
|
||||||
case let .asset(asset):
|
|
||||||
return asset.attachmentType!
|
|
||||||
case .image(_):
|
|
||||||
return .image
|
|
||||||
case .video(_):
|
|
||||||
return .video
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isAsset: Bool {
|
|
||||||
switch self {
|
|
||||||
case .asset(_):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var canSaveToDraft: Bool {
|
|
||||||
switch self {
|
|
||||||
case .video(_):
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getData(completion: @escaping (Data, String) -> Void) {
|
|
||||||
switch self {
|
|
||||||
case let .image(image):
|
|
||||||
completion(image.pngData()!, "image/png")
|
|
||||||
case let .asset(asset):
|
|
||||||
if asset.mediaType == .image {
|
|
||||||
let options = PHImageRequestOptions()
|
|
||||||
options.version = .current
|
|
||||||
options.deliveryMode = .highQualityFormat
|
|
||||||
options.resizeMode = .none
|
|
||||||
options.isNetworkAccessAllowed = true
|
|
||||||
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in
|
|
||||||
guard var data = data, let dataUTI = dataUTI else { fatalError() }
|
|
||||||
|
|
||||||
let mimeType: String
|
|
||||||
if dataUTI == "public.heic" {
|
|
||||||
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
|
|
||||||
let image = CIImage(data: data)!
|
|
||||||
let context = CIContext()
|
|
||||||
let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
|
|
||||||
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
|
||||||
mimeType = "image/jpeg"
|
|
||||||
} else {
|
|
||||||
mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String
|
|
||||||
}
|
|
||||||
|
|
||||||
completion(data, mimeType)
|
|
||||||
}
|
|
||||||
} else if asset.mediaType == .video {
|
|
||||||
let options = PHVideoRequestOptions()
|
|
||||||
options.deliveryMode = .automatic
|
|
||||||
options.isNetworkAccessAllowed = true
|
|
||||||
options.version = .current
|
|
||||||
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
|
||||||
guard let exportSession = exportSession else { fatalError("failed to create export session") }
|
|
||||||
CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fatalError("assetType must be either image or video")
|
|
||||||
}
|
|
||||||
case let .video(url):
|
|
||||||
let asset = AVURLAsset(url: url)
|
|
||||||
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
|
||||||
fatalError("failed to create export session")
|
|
||||||
}
|
|
||||||
CompositionAttachmentData.exportVideoData(session: session, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) {
|
|
||||||
session.outputFileType = .mp4
|
|
||||||
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
|
||||||
session.exportAsynchronously {
|
|
||||||
guard session.status == .completed else { fatalError("video export failed: \(String(describing: session.error))") }
|
|
||||||
do {
|
|
||||||
let data = try Data(contentsOf: session.outputURL!)
|
|
||||||
completion(data, "video/mp4")
|
|
||||||
} catch {
|
|
||||||
fatalError("Unable to load video: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AttachmentType {
|
|
||||||
case image, video
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PHAsset {
|
|
||||||
var attachmentType: CompositionAttachmentData.AttachmentType? {
|
|
||||||
switch self.mediaType {
|
|
||||||
case .image:
|
|
||||||
return .image
|
|
||||||
case .video:
|
|
||||||
return .video
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CompositionAttachmentData: Codable {
|
|
||||||
func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
switch self {
|
|
||||||
case let .asset(asset):
|
|
||||||
try container.encode("asset", forKey: .type)
|
|
||||||
try container.encode(asset.localIdentifier, forKey: .assetIdentifier)
|
|
||||||
case let .image(image):
|
|
||||||
try container.encode("image", forKey: .type)
|
|
||||||
try container.encode(image.pngData()!, forKey: .imageData)
|
|
||||||
case .video(_):
|
|
||||||
throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "video CompositionAttachments cannot be encoded"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
switch try container.decode(String.self, forKey: .type) {
|
|
||||||
case "asset":
|
|
||||||
let identifier = try container.decode(String.self, forKey: .assetIdentifier)
|
|
||||||
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else {
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: .assetIdentifier, in: container, debugDescription: "Could not fetch asset with local identifier")
|
|
||||||
}
|
|
||||||
self = .asset(asset)
|
|
||||||
case "image":
|
|
||||||
guard let image = UIImage(data: try container.decode(Data.self, forKey: .imageData)) else {
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "Could not decode UIImage from image data")
|
|
||||||
}
|
|
||||||
self = .image(image)
|
|
||||||
default:
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of 'image' or 'asset'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CodingKeys: CodingKey {
|
|
||||||
case type
|
|
||||||
case imageData
|
|
||||||
/// The local identifier of the PHAsset for this attachment
|
|
||||||
case assetIdentifier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CompositionAttachmentData: Equatable {
|
|
||||||
static func ==(lhs: CompositionAttachmentData, rhs: CompositionAttachmentData) -> Bool {
|
|
||||||
switch (lhs, rhs) {
|
|
||||||
case let (.asset(a), .asset(b)):
|
|
||||||
return a.localIdentifier == b.localIdentifier
|
|
||||||
case let (.image(a), .image(b)):
|
|
||||||
return a == b
|
|
||||||
case let (.video(a), .video(b)):
|
|
||||||
return a == b
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -46,6 +46,7 @@ extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegat
|
||||||
func didSelectInstance(url: URL) {
|
func didSelectInstance(url: URL) {
|
||||||
let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!)
|
let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!)
|
||||||
instanceTimelineController.delegate = instanceTimelineDelegate
|
instanceTimelineController.delegate = instanceTimelineDelegate
|
||||||
|
instanceTimelineController.browsingEnabled = false
|
||||||
show(instanceTimelineController, sender: self)
|
show(instanceTimelineController, sender: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@ class ProfileViewController: UIPageViewController {
|
||||||
|
|
||||||
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
||||||
headerView.updateUI(for: accountID)
|
headerView.updateUI(for: accountID)
|
||||||
|
updateAccountUI()
|
||||||
} else {
|
} else {
|
||||||
let req = Client.getAccount(id: accountID)
|
let req = Client.getAccount(id: accountID)
|
||||||
mastodonController.run(req) { [weak self] (response) in
|
mastodonController.run(req) { [weak self] (response) in
|
||||||
|
@ -93,6 +94,7 @@ class ProfileViewController: UIPageViewController {
|
||||||
guard case let .success(account, _) = response else { fatalError() }
|
guard case let .success(account, _) = response else { fatalError() }
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (account) in
|
self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (account) in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.updateAccountUI()
|
||||||
self.headerView.updateUI(for: self.accountID)
|
self.headerView.updateUI(for: self.accountID)
|
||||||
self.pageControllers.forEach {
|
self.pageControllers.forEach {
|
||||||
$0.updateUI(account: account)
|
$0.updateUI(account: account)
|
||||||
|
@ -209,7 +211,7 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||||
|
|
||||||
func showActivityController(activities: [UIActivity]) {
|
func showActivityController(activities: [UIActivity]) {
|
||||||
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities)
|
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities)
|
||||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: account.url)
|
||||||
activityController.popoverPresentationController?.sourceView = sourceView
|
activityController.popoverPresentationController?.sourceView = sourceView
|
||||||
self.present(activityController, animated: true)
|
self.present(activityController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ class InstanceTimelineViewController: TimelineTableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var browsingEnabled = true
|
||||||
|
|
||||||
init(for url: URL, parentMastodonController: MastodonController) {
|
init(for url: URL, parentMastodonController: MastodonController) {
|
||||||
self.parentMastodonController = parentMastodonController
|
self.parentMastodonController = parentMastodonController
|
||||||
|
|
||||||
|
@ -66,36 +68,15 @@ class InstanceTimelineViewController: TimelineTableViewController {
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell = super.tableView(tableView, cellForRowAt: indexPath) as! TimelineStatusTableViewCell
|
let cell = super.tableView(tableView, cellForRowAt: indexPath) as! TimelineStatusTableViewCell
|
||||||
cell.delegate = nil
|
cell.delegate = browsingEnabled ? self : nil
|
||||||
cell.overrideMastodonController = mastodonController
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
// no-op, we don't currently support viewing whole conversations from other instances
|
guard browsingEnabled else { return }
|
||||||
}
|
super.tableView(tableView, didSelectRowAt: indexPath)
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
// don't show other screens or actions for other instances
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
// don't show swipe actions for other instances
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
// only show more actions for other instances
|
|
||||||
let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in
|
|
||||||
completion(true)
|
|
||||||
self.showMoreOptions(forStatus: self.timelineSegments[indexPath.section][indexPath.row].id, sourceView: tableView.cellForRow(at: indexPath))
|
|
||||||
}
|
|
||||||
more.image = UIImage(systemName: "ellipsis.circle.fill")
|
|
||||||
more.backgroundColor = .lightGray
|
|
||||||
return UISwipeActionsConfiguration(actions: [more])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class TimelineTableViewController: EnhancedTableViewController {
|
class TimelineTableViewController: EnhancedTableViewController, StatusTableViewCellDelegate {
|
||||||
|
|
||||||
var timeline: Timeline!
|
var timeline: Timeline!
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
@ -206,7 +206,7 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func refreshStatuses(_ sender: Any) {
|
@objc func refreshStatuses(_ sender: Any) {
|
||||||
guard let newer = newer else { return }
|
guard let newer = newer else { return }
|
||||||
|
@ -244,16 +244,18 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
compose()
|
compose()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// MARK: - TuskerNavigationDelegate
|
||||||
|
|
||||||
extension TimelineTableViewController: StatusTableViewCellDelegate {
|
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
|
|
||||||
|
// MARK: - StatusTableViewCellDelegate
|
||||||
|
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
// causes the table view to recalculate the cell heights
|
// causes the table view to recalculate the cell heights
|
||||||
tableView.beginUpdates()
|
tableView.beginUpdates()
|
||||||
tableView.endUpdates()
|
tableView.endUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||||
|
|
|
@ -37,6 +37,16 @@ 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 {
|
||||||
|
return [
|
||||||
|
openInSafariAction(url: account.url),
|
||||||
|
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
var actionsSection: [UIMenuElement] = [
|
var actionsSection: [UIMenuElement] = [
|
||||||
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { [weak self] (_) in
|
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
@ -123,6 +133,17 @@ extension MenuPreviewProvider {
|
||||||
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIMenuElement] {
|
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIMenuElement] {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) else { return [] }
|
let status = mastodonController.persistentContainer.status(for: statusID) else { return [] }
|
||||||
|
|
||||||
|
guard mastodonController.loggedIn else {
|
||||||
|
return [
|
||||||
|
openInSafariAction(url: status.url!),
|
||||||
|
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.navigationDelegate?.showMoreOptions(forStatus: statusID, sourceView: sourceView)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
let bookmarked = status.bookmarked ?? false
|
let bookmarked = status.bookmarked ?? false
|
||||||
let muted = status.muted
|
let muted = status.muted
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ extension TuskerNavigationDelegate {
|
||||||
OpenInSafariActivity()
|
OpenInSafariActivity()
|
||||||
]
|
]
|
||||||
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: customActivites)
|
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: customActivites)
|
||||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: url)
|
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: url)
|
||||||
return activityController
|
return activityController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ extension TuskerNavigationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
let activityController = UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: customActivites)
|
let activityController = UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: customActivites)
|
||||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: url)
|
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: url)
|
||||||
return activityController
|
return activityController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ extension TuskerNavigationDelegate {
|
||||||
]
|
]
|
||||||
|
|
||||||
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: customActivities)
|
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: customActivities)
|
||||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: account.url)
|
||||||
return activityController
|
return activityController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Gifu
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
protocol AttachmentViewDelegate: class {
|
protocol AttachmentViewDelegate: class {
|
||||||
func attachmentViewGallery(startingAt index: Int) -> UIViewController
|
func attachmentViewGallery(startingAt index: Int) -> UIViewController?
|
||||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,8 +230,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func showGallery() {
|
func showGallery() {
|
||||||
if let delegate = delegate {
|
if let delegate = delegate,
|
||||||
let gallery = delegate.attachmentViewGallery(startingAt: index)
|
let gallery = delegate.attachmentViewGallery(startingAt: index) {
|
||||||
delegate.attachmentViewPresent(gallery, animated: true)
|
delegate.attachmentViewPresent(gallery, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -384,8 +384,8 @@ class AttachmentsContainerView: UIView {
|
||||||
@objc func moreViewTapped() {
|
@objc func moreViewTapped() {
|
||||||
guard attachments.count > 4 else { return }
|
guard attachments.count > 4 else { return }
|
||||||
// the more view shows up in place of the fourth attachemtn view, show tapping it should start at the fourth attachment
|
// the more view shows up in place of the fourth attachemtn view, show tapping it should start at the fourth attachment
|
||||||
if let delegate = delegate {
|
if let delegate = delegate,
|
||||||
let gallery = delegate.attachmentViewGallery(startingAt: 3)
|
let gallery = delegate.attachmentViewGallery(startingAt: 3) {
|
||||||
delegate.attachmentViewPresent(gallery, animated: true)
|
delegate.attachmentViewPresent(gallery, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ class ProfileHeaderView: UIView {
|
||||||
noteTextView.setEmojis(account.emojis)
|
noteTextView.setEmojis(account.emojis)
|
||||||
|
|
||||||
// don't show relationship label for the user's own account
|
// don't show relationship label for the user's own account
|
||||||
if accountID != mastodonController.account.id {
|
if accountID != mastodonController.account?.id {
|
||||||
let request = Client.getRelationships(accounts: [accountID])
|
let request = Client.getRelationships(accounts: [accountID])
|
||||||
mastodonController.run(request) { [weak self] (response) in
|
mastodonController.run(request) { [weak self] (response) in
|
||||||
guard let self = self,
|
guard let self = self,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17147" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17120"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||||
<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"/>
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bRJ-Xf-kc9" customClass="VisualEffectImageButton" customModule="Tusker" customModuleProvider="target">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bRJ-Xf-kc9" customClass="VisualEffectImageButton" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="374" y="154" width="32" height="32"/>
|
<rect key="frame" x="374" y="154" width="32" height="32"/>
|
||||||
<viewLayoutGuide key="safeArea" id="kQa-ou-pQz"/>
|
<viewLayoutGuide key="safeArea" id="kQa-ou-pQz"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<accessibility key="accessibilityConfiguration" label="More Actions">
|
||||||
<accessibilityTraits key="traits" button="YES"/>
|
<accessibilityTraits key="traits" button="YES"/>
|
||||||
</accessibility>
|
</accessibility>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
|
|
@ -162,7 +162,10 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
// Pleroma allows 'Boost to original audience' for your own private posts
|
// Pleroma allows 'Boost to original audience' for your own private posts
|
||||||
reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account.id)
|
reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account.id)
|
||||||
}
|
}
|
||||||
reblogButton.isEnabled = !reblogDisabled
|
reblogButton.isEnabled = !reblogDisabled && mastodonController.loggedIn
|
||||||
|
|
||||||
|
favoriteButton.isEnabled = mastodonController.loggedIn
|
||||||
|
replyButton.isEnabled = mastodonController.loggedIn
|
||||||
|
|
||||||
updateStatusIconsForPreferences(status)
|
updateStatusIconsForPreferences(status)
|
||||||
|
|
||||||
|
@ -366,10 +369,11 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BaseStatusTableViewCell: AttachmentViewDelegate {
|
extension BaseStatusTableViewCell: AttachmentViewDelegate {
|
||||||
func attachmentViewGallery(startingAt index: Int) -> UIViewController {
|
func attachmentViewGallery(startingAt index: Int) -> UIViewController? {
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
guard let delegate = delegate,
|
||||||
|
let status = mastodonController.persistentContainer.status(for: statusID) else { return nil }
|
||||||
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
|
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
|
||||||
return delegate!.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
return delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
||||||
|
|
|
@ -196,7 +196,8 @@ extension TimelineStatusTableViewCell: SelectableTableViewCell {
|
||||||
extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
|
|
||||||
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
guard let mastodonController = mastodonController,
|
||||||
|
mastodonController.loggedIn else { return nil }
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||||
|
|
||||||
let favoriteTitle: String
|
let favoriteTitle: String
|
||||||
|
@ -258,13 +259,6 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
||||||
let reply = UIContextualAction(style: .normal, title: "Reply") { (action, view, completion) in
|
|
||||||
completion(true)
|
|
||||||
self.reply()
|
|
||||||
}
|
|
||||||
reply.image = UIImage(systemName: "arrowshape.turn.up.left.fill")
|
|
||||||
reply.backgroundColor = tintColor
|
|
||||||
|
|
||||||
let moreTitle: String
|
let moreTitle: String
|
||||||
let moreImage: UIImage
|
let moreImage: UIImage
|
||||||
// on iOS 14+, more actions are in the context menu so display this as 'Share'
|
// on iOS 14+, more actions are in the context menu so display this as 'Share'
|
||||||
|
@ -284,6 +278,18 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
}
|
}
|
||||||
more.image = moreImage
|
more.image = moreImage
|
||||||
more.backgroundColor = .lightGray
|
more.backgroundColor = .lightGray
|
||||||
|
|
||||||
|
guard mastodonController.loggedIn else {
|
||||||
|
return UISwipeActionsConfiguration(actions: [more])
|
||||||
|
}
|
||||||
|
|
||||||
|
let reply = UIContextualAction(style: .normal, title: "Reply") { (action, view, completion) in
|
||||||
|
completion(true)
|
||||||
|
self.reply()
|
||||||
|
}
|
||||||
|
reply.image = UIImage(systemName: "arrowshape.turn.up.left.fill")
|
||||||
|
reply.backgroundColor = tintColor
|
||||||
|
|
||||||
return UISwipeActionsConfiguration(actions: [reply, more])
|
return UISwipeActionsConfiguration(actions: [reply, more])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,9 +54,8 @@ extension Router {
|
||||||
]
|
]
|
||||||
self["/api/v1/accounts/verify_credentials"] = JSONResponse(result: selfAccount)
|
self["/api/v1/accounts/verify_credentials"] = JSONResponse(result: selfAccount)
|
||||||
self["/api/v1/accounts/\\d+/statuses"] = JSONResponse(result: [])
|
self["/api/v1/accounts/\\d+/statuses"] = JSONResponse(result: [])
|
||||||
self["/api/v1/accounts/(\\d+)"] = DelegatingResponse { (env) in
|
self["/api/v1/accounts/(\\d+)"] = DelegatingResponse { (ctx) in
|
||||||
let captures = env["ambassador.router_captures"] as! [String]
|
if ctx.captures[0] == "1" {
|
||||||
if captures[0] == "1" {
|
|
||||||
return JSONResponse(result: selfAccount)
|
return JSONResponse(result: selfAccount)
|
||||||
} else {
|
} else {
|
||||||
return JSONResponse(statusCode: 404, statusMessage: "Not Found", result: notFound)
|
return JSONResponse(statusCode: 404, statusMessage: "Not Found", result: notFound)
|
||||||
|
|
|
@ -10,9 +10,20 @@ import Foundation
|
||||||
import Ambassador
|
import Ambassador
|
||||||
|
|
||||||
struct DelegatingResponse: WebApp {
|
struct DelegatingResponse: WebApp {
|
||||||
let handler: (_ environ: [String: Any]) -> WebApp
|
let handler: (_ ctx: Context) -> WebApp
|
||||||
|
|
||||||
func app(_ environ: [String : Any], startResponse: @escaping ((String, [(String, String)]) -> Void), sendBody: @escaping ((Data) -> Void)) {
|
func app(_ environ: [String : Any], startResponse: @escaping ((String, [(String, String)]) -> Void), sendBody: @escaping ((Data) -> Void)) {
|
||||||
handler(environ).app(environ, startResponse: startResponse, sendBody: sendBody)
|
let ctx = Context(environ: environ)
|
||||||
|
handler(ctx).app(environ, startResponse: startResponse, sendBody: sendBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DelegatingResponse {
|
||||||
|
struct Context {
|
||||||
|
let environ: [String: Any]
|
||||||
|
|
||||||
|
var captures: [String] {
|
||||||
|
environ["ambassador.router_captures"] as? [String] ?? []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
//
|
||||||
|
// ComposeTests.swift
|
||||||
|
// TuskerUITests
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/12/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class ComposeTests: TuskerUITests {
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
|
||||||
|
router.allRoutes()
|
||||||
|
|
||||||
|
app.launchEnvironment["UI_TESTING_LOGIN"] = "true"
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
app.tabBars.buttons["Compose"].tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCurrentAccount() {
|
||||||
|
XCTAssertTrue(app.images["Admin Account avatar"].exists, "avatar image exists")
|
||||||
|
XCTAssertTrue(app.staticTexts["Admin Account"].exists, "display name label exists")
|
||||||
|
XCTAssertTrue(app.staticTexts["@admin"].exists, "acct label exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBodyPlaceholder() {
|
||||||
|
XCTAssertTrue(app.staticTexts["What's on your mind?"].exists, "placeholder exists")
|
||||||
|
app.textViews.firstMatch.typeText("Blah")
|
||||||
|
XCTAssertFalse(app.staticTexts["What's on your mind?"].exists, "placeholder does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCharacterCounter() {
|
||||||
|
XCTAssertTrue(app.staticTexts["500 characters remaining"].exists, "initial character count is 500")
|
||||||
|
let textView = app.textViews.firstMatch
|
||||||
|
|
||||||
|
let fragments = [
|
||||||
|
"Hello",
|
||||||
|
"World",
|
||||||
|
"@admin",
|
||||||
|
"@admin@example.com",
|
||||||
|
"https://foo.example.com/?bar=baz#qux",
|
||||||
|
]
|
||||||
|
|
||||||
|
var remaining = 500
|
||||||
|
for s in fragments {
|
||||||
|
let length = CharacterCounter.count(text: s)
|
||||||
|
// add 1 for newline
|
||||||
|
remaining -= length + 1
|
||||||
|
|
||||||
|
textView.typeText(s + "\n")
|
||||||
|
XCTAssertTrue(app.staticTexts["\(remaining) characters remaining"].exists, "subsequent character count is \(remaining)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func testToolbarSwitching() {
|
||||||
|
// // text view is automatically focused, so unfocus
|
||||||
|
// app.swipeDown()
|
||||||
|
//
|
||||||
|
// XCTAssertEqual(app.toolbars.count, 1)
|
||||||
|
// XCTAssertEqual(app.toolbars.buttons.count, 3)
|
||||||
|
// XCTAssertTrue(app.toolbars.buttons["CW"].exists)
|
||||||
|
// XCTAssertTrue(app.toolbars.buttons["Visibility: Public"].exists)
|
||||||
|
// XCTAssertTrue(app.toolbars.buttons["Drafts"].exists)
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
func testContentWarning() {
|
||||||
|
let toolbar = app.toolbars.firstMatch
|
||||||
|
XCTAssertTrue(toolbar.waitForExistence(timeout: 0.1))
|
||||||
|
XCTAssertEqual(app.toolbars.count, 1, "there is only 1 toolbar")
|
||||||
|
let cwButton = toolbar.buttons["CW"]
|
||||||
|
XCTAssertTrue(cwButton.exists, "the CW button exists")
|
||||||
|
|
||||||
|
cwButton.tap()
|
||||||
|
let cwField = app.textFields.firstMatch
|
||||||
|
XCTAssertEqual(cwField.placeholderValue, "Write your warning here")
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["500 characters remaining"].exists)
|
||||||
|
cwField.tap()
|
||||||
|
let str: String
|
||||||
|
// on iOS 14, the first type text is typed into a SwiftUI TextField, the 2nd character is inexplicably dropped >.<
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
str = "fooo"
|
||||||
|
} else {
|
||||||
|
str = "foo"
|
||||||
|
}
|
||||||
|
cwField.typeText(str)
|
||||||
|
XCTAssertTrue(app.staticTexts["497 characters remaining"].exists)
|
||||||
|
// CharacterCounter is not used => '@' is counted
|
||||||
|
cwField.typeText(" @bar")
|
||||||
|
XCTAssertTrue(app.staticTexts["492 characters remaining"].exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,8 +19,55 @@ class MyProfileTests: TuskerUITests {
|
||||||
app.launch()
|
app.launch()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testExample() {
|
func testProfileHeader() {
|
||||||
sleep(10000000)
|
app.tabBars.buttons["My Profile"].tap()
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["Admin Account"].exists)
|
||||||
|
XCTAssertTrue(app.staticTexts["@admin"].exists)
|
||||||
|
XCTAssertTrue(app.staticTexts["My profile description."].exists)
|
||||||
|
|
||||||
|
let segmented = app.segmentedControls.firstMatch
|
||||||
|
XCTAssertTrue(segmented.exists)
|
||||||
|
XCTAssertEqual(segmented.buttons.count, 3)
|
||||||
|
XCTAssertTrue(segmented.buttons["Posts"].exists)
|
||||||
|
XCTAssertTrue(segmented.buttons["Posts and Replies"].exists)
|
||||||
|
XCTAssertTrue(segmented.buttons["Media"].exists)
|
||||||
|
|
||||||
|
XCTAssertTrue(app.buttons["More Actions"].exists)
|
||||||
|
|
||||||
|
app.buttons["More Actions"].tap()
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
XCTAssertTrue(app.buttons["Open in Safari"].exists)
|
||||||
|
XCTAssertTrue(app.buttons["Share..."].exists)
|
||||||
|
XCTAssertTrue(app.buttons["Send Message"].exists)
|
||||||
|
|
||||||
|
app.buttons["Open in Safari"].tap()
|
||||||
|
XCTAssertTrue(app.otherElements["TopBrowserBar"].exists)
|
||||||
|
app.buttons["Done"].tap()
|
||||||
|
XCTAssertFalse(app.otherElements["TopBrowserBar"].exists)
|
||||||
|
|
||||||
|
app.buttons["More Actions"].tap()
|
||||||
|
app.buttons["Share..."].tap()
|
||||||
|
let activityListView = app.otherElements["ActivityListView"]
|
||||||
|
XCTAssertTrue(activityListView.waitForExistence(timeout: 0.2))
|
||||||
|
activityListView.buttons["Close"].tap()
|
||||||
|
XCTAssertFalse(activityListView.exists)
|
||||||
|
|
||||||
|
app.buttons["More Actions"].tap()
|
||||||
|
app.buttons["Send Message"].tap()
|
||||||
|
XCTAssertTrue(app.staticTexts["Compose"].exists)
|
||||||
|
XCTAssertTrue(app.buttons["Cancel"].exists)
|
||||||
|
XCTAssertTrue(app.buttons["Post"].exists)
|
||||||
|
app.buttons["Cancel"].tap()
|
||||||
|
} else {
|
||||||
|
// first tap doesn't trigger share sheet for some reason
|
||||||
|
app.buttons["More Actions"].tap()
|
||||||
|
let activityListView = app.otherElements["ActivityListView"]
|
||||||
|
XCTAssertTrue(activityListView.waitForExistence(timeout: 0.2))
|
||||||
|
activityListView.buttons["Close"].tap()
|
||||||
|
XCTAssertFalse(activityListView.exists)
|
||||||
|
// can't test individual actions :/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,22 @@ class OnboardingTests: TuskerUITests {
|
||||||
|
|
||||||
// can't test logging in because there's no way of interacting with the safari VC that's used in the OAuth flow
|
// can't test logging in because there's no way of interacting with the safari VC that's used in the OAuth flow
|
||||||
|
|
||||||
|
func testSearchSuggestedInstances() {
|
||||||
|
XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 2))
|
||||||
|
let firstInstanceLabel = app.cells.staticTexts.firstMatch.label
|
||||||
|
let secondInstanceLabel = app.cells.element(boundBy: 1).staticTexts.firstMatch.label
|
||||||
|
let searchField = app.searchFields.element
|
||||||
|
searchField.tap()
|
||||||
|
searchField.typeText(firstInstanceLabel)
|
||||||
|
XCTAssertTrue(app.staticTexts[firstInstanceLabel].exists)
|
||||||
|
XCTAssertFalse(app.staticTexts[secondInstanceLabel].exists)
|
||||||
|
}
|
||||||
|
|
||||||
func testCustomInstanceAppears() {
|
func testCustomInstanceAppears() {
|
||||||
let searchField = app.searchFields.element
|
let searchField = app.searchFields.element
|
||||||
searchField.tap()
|
searchField.tap()
|
||||||
searchField.typeText("http://localhost:8080")
|
searchField.typeText("http://localhost:8080")
|
||||||
XCTAssertTrue(app.staticTexts["localhost"].exists)
|
XCTAssertTrue(app.staticTexts["localhost"].waitForExistence(timeout: 2))
|
||||||
XCTAssertTrue(app.staticTexts["An instance description"].exists)
|
XCTAssertTrue(app.staticTexts["An instance description"].exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue