Compare commits
8 Commits
3ea1ad5622
...
abe2bbdfd4
Author | SHA1 | Date |
---|---|---|
Shadowfacts | abe2bbdfd4 | |
Shadowfacts | 1d9efc7fb5 | |
Shadowfacts | b17b7b7a24 | |
Shadowfacts | 18d7917756 | |
Shadowfacts | cc401fce8c | |
Shadowfacts | a5fc35d0b1 | |
Shadowfacts | acd48a6db4 | |
Shadowfacts | b45d3fb80a |
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,5 +1,15 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2023.1 (60)
|
||||||
|
Features/Improvements:
|
||||||
|
- Allow sharing gifv attachments
|
||||||
|
- Add email subject when sharing status/account
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash when inserting present items after navigating away from and returning to timeline
|
||||||
|
- Fix error decoding statuses from certain instances
|
||||||
|
- When logging out, remove the scene's active account rather than the most recently activated
|
||||||
|
|
||||||
## 2022.1 (59)
|
## 2022.1 (59)
|
||||||
This build is a hotfix for a crash when migrating saved instances to iCloud.
|
This build is a hotfix for a crash when migrating saved instances to iCloud.
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
guard response.statusCode == 200 else {
|
guard response.statusCode == 200 else {
|
||||||
let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data)
|
let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data)
|
||||||
let type: ErrorType = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode)
|
let type: ErrorType = mastodonError.flatMap { .mastodonError(response.statusCode, $0.description) } ?? .unexpectedStatus(response.statusCode)
|
||||||
completion(.failure(Error(request: request, type: type)))
|
completion(.failure(Error(request: request, type: type)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -462,8 +462,8 @@ extension Client {
|
||||||
return "Invalid Response"
|
return "Invalid Response"
|
||||||
case .invalidModel(_):
|
case .invalidModel(_):
|
||||||
return "Invalid Model"
|
return "Invalid Model"
|
||||||
case .mastodonError(let error):
|
case .mastodonError(let code, let error):
|
||||||
return "Server Error: \(error)"
|
return "Server Error (\(code)): \(error)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -473,6 +473,6 @@ extension Client {
|
||||||
case invalidRequest
|
case invalidRequest
|
||||||
case invalidResponse
|
case invalidResponse
|
||||||
case invalidModel(Swift.Error)
|
case invalidModel(Swift.Error)
|
||||||
case mastodonError(String)
|
case mastodonError(Int, String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,12 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import WebURL
|
||||||
|
|
||||||
public final class Status: StatusProtocol, Decodable {
|
public final class Status: StatusProtocol, Decodable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let url: URL?
|
public let url: WebURL?
|
||||||
public let account: Account
|
public let account: Account
|
||||||
public let inReplyToID: String?
|
public let inReplyToID: String?
|
||||||
public let inReplyToAccountID: String?
|
public let inReplyToAccountID: String?
|
||||||
|
|
|
@ -289,6 +289,7 @@
|
||||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
||||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; };
|
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; };
|
||||||
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; };
|
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; };
|
||||||
|
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */; };
|
||||||
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */; };
|
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */; };
|
||||||
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */; };
|
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */; };
|
||||||
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B57292D5B2C00D528E1 /* StatusActionAccountListViewController.swift */; };
|
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B57292D5B2C00D528E1 /* StatusActionAccountListViewController.swift */; };
|
||||||
|
@ -674,6 +675,7 @@
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
|
||||||
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = "<group>"; };
|
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = "<group>"; };
|
||||||
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = "<group>"; };
|
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = "<group>"; };
|
||||||
|
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToPhotosActivity.swift; sourceTree = "<group>"; };
|
||||||
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineGapCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineGapCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D12B57292D5B2C00D528E1 /* StatusActionAccountListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListViewController.swift; sourceTree = "<group>"; };
|
D6D12B57292D5B2C00D528E1 /* StatusActionAccountListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1299,6 +1301,7 @@
|
||||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */,
|
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */,
|
||||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
|
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
|
||||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
|
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
|
||||||
|
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */,
|
||||||
);
|
);
|
||||||
path = Activities;
|
path = Activities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1974,6 +1977,7 @@
|
||||||
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
|
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
|
||||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
||||||
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
|
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
|
@ -2282,14 +2286,15 @@
|
||||||
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 = 59;
|
CURRENT_PROJECT_VERSION = 60;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2022.1;
|
MARKETING_VERSION = 2023.1;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -2349,7 +2354,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 59;
|
CURRENT_PROJECT_VERSION = 60;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2358,7 +2363,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2022.1;
|
MARKETING_VERSION = 2023.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -2500,14 +2505,15 @@
|
||||||
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 = 59;
|
CURRENT_PROJECT_VERSION = 60;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2022.1;
|
MARKETING_VERSION = 2023.1;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
OTHER_LDFLAGS = "";
|
OTHER_LDFLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
|
@ -2528,14 +2534,15 @@
|
||||||
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 = 59;
|
CURRENT_PROJECT_VERSION = 60;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2022.1;
|
MARKETING_VERSION = 2023.1;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -2637,7 +2644,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 59;
|
CURRENT_PROJECT_VERSION = 60;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2646,7 +2653,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2022.1;
|
MARKETING_VERSION = 2023.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -2663,7 +2670,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 59;
|
CURRENT_PROJECT_VERSION = 60;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2672,7 +2679,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2022.1;
|
MARKETING_VERSION = 2023.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
|
|
@ -37,4 +37,8 @@ class AccountActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||||
|
return "\(account.displayName) (@\(account.acct)"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
//
|
||||||
|
// SaveToPhotosActivity.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/2/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
import Photos
|
||||||
|
|
||||||
|
class SaveToPhotosActivity: UIActivity {
|
||||||
|
|
||||||
|
override class var activityCategory: UIActivity.Category {
|
||||||
|
return .action
|
||||||
|
}
|
||||||
|
override var activityType: UIActivity.ActivityType? {
|
||||||
|
return .saveToPhotos
|
||||||
|
}
|
||||||
|
|
||||||
|
override var activityTitle: String? {
|
||||||
|
return "Save to Photos"
|
||||||
|
}
|
||||||
|
override var activityImage: UIImage? {
|
||||||
|
UIImage(systemName: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||||
|
return activityItems.contains(where: {
|
||||||
|
if let url = $0 as? URL,
|
||||||
|
let type = UTType(filenameExtension: url.pathExtension){
|
||||||
|
return type.conforms(to: .movie) || type.conforms(to: .video) || type.conforms(to: .image)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private var url: URL?
|
||||||
|
private var type: UTType?
|
||||||
|
|
||||||
|
override func prepare(withActivityItems activityItems: [Any]) {
|
||||||
|
for case let url as URL in activityItems {
|
||||||
|
self.url = url
|
||||||
|
type = UTType(filenameExtension: url.pathExtension)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func perform() {
|
||||||
|
guard let url,
|
||||||
|
let type else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
|
||||||
|
guard case .authorized = status else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PHPhotoLibrary.shared().performChanges {
|
||||||
|
if type.conforms(to: .movie) || type.conforms(to: .video) {
|
||||||
|
_ = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
|
||||||
|
} else {
|
||||||
|
_ = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
|
||||||
|
}
|
||||||
|
} completionHandler: { _, _ in
|
||||||
|
self.activityDidFinish(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -39,5 +39,9 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
}
|
}
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||||
|
return "Post by @\(status.account.acct)"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,6 @@ import UIKit
|
||||||
extension UIActivity.ActivityType {
|
extension UIActivity.ActivityType {
|
||||||
|
|
||||||
static let openInSafari = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).open_in_safari")
|
static let openInSafari = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).open_in_safari")
|
||||||
|
static let saveToPhotos = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).save_to_photos")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
@objc(StatusMO)
|
@objc(StatusMO)
|
||||||
public final class StatusMO: NSManagedObject, StatusProtocol {
|
public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
|
@ -127,7 +128,7 @@ extension StatusMO {
|
||||||
self.sensitive = status.sensitive
|
self.sensitive = status.sensitive
|
||||||
self.spoilerText = status.spoilerText
|
self.spoilerText = status.spoilerText
|
||||||
self.uri = status.uri
|
self.uri = status.uri
|
||||||
self.url = status.url
|
self.url = status.url != nil ? URL(status.url!) : nil
|
||||||
self.visibility = status.visibility
|
self.visibility = status.visibility
|
||||||
self.poll = status.poll
|
self.poll = status.poll
|
||||||
self.localOnly = status.localOnly ?? false
|
self.localOnly = status.localOnly ?? false
|
||||||
|
|
|
@ -211,7 +211,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutCurrent() {
|
func logoutCurrent() {
|
||||||
LocalData.shared.removeAccount(LocalData.shared.getMostRecentAccount()!)
|
guard let account = window?.windowScene?.session.mastodonController?.accountInfo else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
LocalData.shared.removeAccount(account)
|
||||||
if LocalData.shared.onboardingComplete {
|
if LocalData.shared.onboardingComplete {
|
||||||
activateAccount(LocalData.shared.accounts.first!, animated: false)
|
activateAccount(LocalData.shared.accounts.first!, animated: false)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -15,7 +15,7 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
private let accountIDs: [String]
|
private let accountIDs: [String]
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
var collectionView: UICollectionView! {
|
||||||
view as! UICollectionView
|
view as? UICollectionView
|
||||||
}
|
}
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
|
|
|
@ -31,13 +31,20 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
||||||
|
|
||||||
var animationImage: UIImage? { image! }
|
var animationImage: UIImage? { image! }
|
||||||
var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
[image!]
|
guard let data else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [ImageActivityItemSource(data: data, url: url, image: image)]
|
||||||
}
|
}
|
||||||
weak var owner: LargeImageViewController?
|
weak var owner: LargeImageViewController?
|
||||||
|
|
||||||
private var sourceData: Data?
|
private let url: URL
|
||||||
|
private let data: Data?
|
||||||
|
|
||||||
init(image: UIImage) {
|
init(url: URL, data: Data?, image: UIImage) {
|
||||||
|
self.url = url
|
||||||
|
self.data = data
|
||||||
|
|
||||||
super.init(image: image)
|
super.init(image: image)
|
||||||
|
|
||||||
contentMode = .scaleAspectFit
|
contentMode = .scaleAspectFit
|
||||||
|
@ -79,7 +86,7 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
func grayscaleStateChanged() {
|
||||||
guard let data = sourceData else {
|
guard let data else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,12 +115,15 @@ extension LargeImageImageContentView: ImageAnalysisInteractionDelegate {
|
||||||
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||||
var animationImage: UIImage? { image }
|
var animationImage: UIImage? { image }
|
||||||
var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
// todo: should gifs share the data?
|
[ImageActivityItemSource(data: gifController!.gifData, url: url, image: image)]
|
||||||
[image].compactMap { $0 }
|
|
||||||
}
|
}
|
||||||
weak var owner: LargeImageViewController?
|
weak var owner: LargeImageViewController?
|
||||||
|
|
||||||
init(gifController: GIFController) {
|
private let url: URL
|
||||||
|
|
||||||
|
init(url: URL, gifController: GIFController) {
|
||||||
|
self.url = url
|
||||||
|
|
||||||
super.init(image: gifController.lastFrame?.image)
|
super.init(image: gifController.lastFrame?.image)
|
||||||
|
|
||||||
contentMode = .scaleAspectFit
|
contentMode = .scaleAspectFit
|
||||||
|
@ -138,22 +148,22 @@ class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||||
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||||
private(set) var animationImage: UIImage?
|
private(set) var animationImage: UIImage?
|
||||||
var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
// todo: what should we share for gifvs?
|
[GifvActivityItemSource(asset: asset, attachment: attachment)]
|
||||||
// some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL?
|
|
||||||
[]
|
|
||||||
}
|
}
|
||||||
weak var owner: LargeImageViewController?
|
weak var owner: LargeImageViewController?
|
||||||
|
|
||||||
|
private let attachment: Attachment
|
||||||
private let asset: AVURLAsset
|
private let asset: AVURLAsset
|
||||||
|
|
||||||
private var videoSize: CGSize?
|
private var videoSize: CGSize?
|
||||||
override var intrinsicContentSize: CGSize {
|
override var intrinsicContentSize: CGSize {
|
||||||
videoSize ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
videoSize ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(attachment: Attachment, source: UIImageView) {
|
init(attachment: Attachment, source: UIImageView) {
|
||||||
precondition(attachment.kind == .gifv)
|
precondition(attachment.kind == .gifv)
|
||||||
|
|
||||||
|
self.attachment = attachment
|
||||||
self.asset = AVURLAsset(url: attachment.url)
|
self.asset = AVURLAsset(url: attachment.url)
|
||||||
|
|
||||||
super.init(asset: asset, gravity: .resizeAspect)
|
super.init(asset: asset, gravity: .resizeAspect)
|
||||||
|
@ -184,4 +194,78 @@ class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||||
func grayscaleStateChanged() {
|
func grayscaleStateChanged() {
|
||||||
// no-op, GifvAttachmentView observes the grayscale state itself
|
// no-op, GifvAttachmentView observes the grayscale state itself
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate class ImageActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
|
let data: Data
|
||||||
|
let url: URL
|
||||||
|
let image: UIImage?
|
||||||
|
|
||||||
|
init(data: Data, url: URL, image: UIImage?) {
|
||||||
|
self.data = data
|
||||||
|
self.url = url
|
||||||
|
self.image = image
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||||
|
do {
|
||||||
|
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
|
try data.write(to: tempURL)
|
||||||
|
return tempURL
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||||
|
return (UTType(filenameExtension: url.pathExtension) ?? .image).identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate class GifvActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
|
let asset: AVAsset
|
||||||
|
let attachment: Attachment
|
||||||
|
|
||||||
|
init(asset: AVAsset, attachment: Attachment) {
|
||||||
|
self.asset = asset
|
||||||
|
self.attachment = attachment
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||||
|
return attachment.url
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
|
||||||
|
let generator = AVAssetImageGenerator(asset: self.asset)
|
||||||
|
generator.appliesPreferredTrackTransform = true
|
||||||
|
if let image = try? generator.copyCGImage(at: .zero, actualTime: nil) {
|
||||||
|
return UIImage(cgImage: image)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: attachment.url)
|
||||||
|
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.url.lastPathComponent)
|
||||||
|
try data.write(to: tempURL)
|
||||||
|
return tempURL
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||||
|
return (UTType(filenameExtension: attachment.url.pathExtension) ?? .video).identifier
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -381,7 +381,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func sharePressed(_ sender: Any) {
|
@IBAction func sharePressed(_ sender: Any) {
|
||||||
let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: nil)
|
let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: [SaveToPhotosActivity()])
|
||||||
activityVC.popoverPresentationController?.sourceView = shareImage
|
activityVC.popoverPresentationController?.sourceView = shareImage
|
||||||
present(activityVC, animated: true)
|
present(activityVC, animated: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,12 +135,12 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
// is it possible for the source view's gif controller to have different data than we just got?
|
// is it possible for the source view's gif controller to have different data than we just got?
|
||||||
// should this be a property set by the animation controller instead?
|
// should this be a property set by the animation controller instead?
|
||||||
let gifController = (animationSourceView as? GIFImageView)?.gifController ?? GIFController(gifData: data)
|
let gifController = (animationSourceView as? GIFImageView)?.gifController ?? GIFController(gifData: data)
|
||||||
content = LargeImageGifContentView(gifController: gifController)
|
content = LargeImageGifContentView(url: url, gifController: gifController)
|
||||||
} else {
|
} else {
|
||||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||||
content = LargeImageImageContentView(image: transformedImage)
|
content = LargeImageImageContentView(url: url, data: data, image: transformedImage)
|
||||||
} else {
|
} else {
|
||||||
content = LargeImageImageContentView(image: image)
|
content = LargeImageImageContentView(url: url, data: data, image: image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
||||||
image = grayscale
|
image = grayscale
|
||||||
}
|
}
|
||||||
setContent(LargeImageImageContentView(image: image))
|
setContent(LargeImageImageContentView(url: url, data: nil, image: image))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -621,9 +621,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
snapshot.insertItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, beforeItem: .gap)
|
snapshot.insertItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, beforeItem: .gap)
|
||||||
|
|
||||||
if applySnapshotBeforeScrolling {
|
if applySnapshotBeforeScrolling {
|
||||||
let firstVisibleIndexPath = collectionView.indexPathsForVisibleItems.min()!
|
if let firstVisibleIndexPath = collectionView.indexPathsForVisibleItems.min() {
|
||||||
let firstVisibleItem = dataSource.itemIdentifier(for: firstVisibleIndexPath)!
|
let firstVisibleItem = dataSource.itemIdentifier(for: firstVisibleIndexPath)!
|
||||||
applySnapshot(snapshot, maintainingBottomRelativeScrollPositionOf: firstVisibleItem)
|
applySnapshot(snapshot, maintainingBottomRelativeScrollPositionOf: firstVisibleItem)
|
||||||
|
} else {
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = ToastConfiguration(title: "Jump to Present")
|
var config = ToastConfiguration(title: "Jump to Present")
|
||||||
|
|
|
@ -57,31 +57,29 @@ extension ToastConfiguration {
|
||||||
}
|
}
|
||||||
// TODO: this is a bizarre place to do this, but code path covers basically all errors
|
// TODO: this is a bizarre place to do this, but code path covers basically all errors
|
||||||
switch error.type {
|
switch error.type {
|
||||||
case .invalidRequest, .invalidResponse, .invalidModel(_), .mastodonError(_):
|
case .invalidRequest, .invalidResponse, .invalidModel(_), .mastodonError(_, _):
|
||||||
SentrySDK.capture(error: error) { scope in
|
let event = Event(error: error)
|
||||||
scope.setFingerprint([String(describing: error)])
|
event.message = SentryMessage(formatted: "\(title): \(error)")
|
||||||
let crumb = Breadcrumb(level: .error, category: "error")
|
event.tags = [
|
||||||
crumb.message = title
|
"request_method": error.requestMethod.name,
|
||||||
crumb.data = [
|
"request_endpoint": error.requestEndpoint.description,
|
||||||
"request_method": error.requestMethod.name,
|
]
|
||||||
"request_endpoint": error.requestEndpoint.description,
|
switch error.type {
|
||||||
]
|
case .invalidRequest:
|
||||||
switch error.type {
|
event.tags!["error_type"] = "invalid_request"
|
||||||
case .invalidRequest:
|
case .invalidResponse:
|
||||||
crumb.data!["error_type"] = "invalid_request"
|
event.tags!["error_type"] = "invalid_response"
|
||||||
case .invalidResponse:
|
case .invalidModel(let error):
|
||||||
crumb.data!["error_type"] = "invalid_response"
|
event.tags!["error_type"] = "invalid_model"
|
||||||
case .invalidModel(let error):
|
event.tags!["underlying_error"] = String(describing: error)
|
||||||
crumb.data!["error_type"] = "invalid_model"
|
case .mastodonError(let code, let error):
|
||||||
crumb.data!["underlying_error"] = String(describing: error)
|
event.tags!["error_type"] = "mastodon_error"
|
||||||
case .mastodonError(let error):
|
event.tags!["response_code"] = "\(code)"
|
||||||
crumb.data!["error_type"] = "mastodon_error"
|
event.tags!["underlying_error"] = error
|
||||||
crumb.data!["underlying_error"] = error
|
default:
|
||||||
default:
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
scope.add(crumb)
|
|
||||||
}
|
}
|
||||||
|
SentrySDK.capture(event: event)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue