Compare commits
No commits in common. "576e4aa90d292426effd295cf3bec1e1f10ca439" and "ee90b20f7fa405b0e5b8d946053cb6e7d2da79b3" have entirely different histories.
576e4aa90d
...
ee90b20f7f
|
@ -173,6 +173,8 @@
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
|
||||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||||
|
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
|
||||||
|
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
||||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
||||||
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; };
|
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; };
|
||||||
|
@ -500,6 +502,8 @@
|
||||||
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||||
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
||||||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||||
|
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
|
||||||
|
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
|
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = "<group>"; };
|
D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1141,6 +1145,15 @@
|
||||||
path = "Account Detail";
|
path = "Account Detail";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D67C57B021E28F9400C3118B /* Compose Status Reply */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */,
|
||||||
|
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */,
|
||||||
|
);
|
||||||
|
path = "Compose Status Reply";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6A3BC7223218C6E00FD64D5 /* Utilities */ = {
|
D6A3BC7223218C6E00FD64D5 /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1260,6 +1273,7 @@
|
||||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||||
|
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
||||||
D626494023C122C800612E6E /* Asset Picker */,
|
D626494023C122C800612E6E /* Asset Picker */,
|
||||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||||
D641C78A213DD926004B4513 /* Status */,
|
D641C78A213DD926004B4513 /* Status */,
|
||||||
|
@ -1631,6 +1645,7 @@
|
||||||
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
||||||
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
|
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
|
||||||
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
|
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
|
||||||
|
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */,
|
||||||
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
||||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||||
|
@ -1803,6 +1818,7 @@
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||||
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||||
|
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
|
||||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||||
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
||||||
|
|
|
@ -89,13 +89,6 @@
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
</CommandLineArguments>
|
</CommandLineArguments>
|
||||||
<EnvironmentVariables>
|
|
||||||
<EnvironmentVariable
|
|
||||||
key = "DISABLE_IMAGE_CACHE"
|
|
||||||
value = "1"
|
|
||||||
isEnabled = "NO">
|
|
||||||
</EnvironmentVariable>
|
|
||||||
</EnvironmentVariables>
|
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|
|
@ -15,7 +15,6 @@ enum Cache<T> {
|
||||||
case disk(DiskStorage<T>)
|
case disk(DiskStorage<T>)
|
||||||
case hybrid(HybridStorage<T>)
|
case hybrid(HybridStorage<T>)
|
||||||
|
|
||||||
@available(*, deprecated, message: "disk-based caches synchronously interact with the file system. Avoid using if possible.")
|
|
||||||
func existsObject(forKey key: String) throws -> Bool {
|
func existsObject(forKey key: String) throws -> Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case let .memory(memory):
|
case let .memory(memory):
|
||||||
|
|
|
@ -16,12 +16,6 @@ class ImageCache {
|
||||||
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
||||||
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
private static let disableCaching = ProcessInfo.processInfo.environment.keys.contains("DISABLE_IMAGE_CACHE")
|
|
||||||
#else
|
|
||||||
private static let disableCaching = false
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private let cache: Cache<Data>
|
private let cache: Cache<Data>
|
||||||
|
|
||||||
private var groups = [URL: RequestGroup]()
|
private var groups = [URL: RequestGroup]()
|
||||||
|
@ -44,7 +38,7 @@ class ImageCache {
|
||||||
|
|
||||||
func get(_ url: URL, completion: ((Data?) -> Void)?) -> Request? {
|
func get(_ url: URL, completion: ((Data?) -> Void)?) -> Request? {
|
||||||
let key = url.absoluteString
|
let key = url.absoluteString
|
||||||
if !ImageCache.disableCaching,
|
if (try? cache.existsObject(forKey: key)) ?? false,
|
||||||
let data = try? cache.object(forKey: key) {
|
let data = try? cache.object(forKey: key) {
|
||||||
completion?(data)
|
completion?(data)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class MastodonController: ObservableObject {
|
class MastodonController {
|
||||||
|
|
||||||
static private(set) var all = [LocalData.UserAccountInfo: MastodonController]()
|
static private(set) var all = [LocalData.UserAccountInfo: MastodonController]()
|
||||||
|
|
||||||
|
@ -42,8 +42,8 @@ class MastodonController: ObservableObject {
|
||||||
|
|
||||||
let client: Client!
|
let client: Client!
|
||||||
|
|
||||||
@Published private(set) var account: Account!
|
var account: Account!
|
||||||
@Published private(set) var instance: Instance!
|
var instance: Instance!
|
||||||
|
|
||||||
var loggedIn: Bool {
|
var loggedIn: Bool {
|
||||||
accountInfo != nil
|
accountInfo != nil
|
||||||
|
@ -95,9 +95,7 @@ class MastodonController: ObservableObject {
|
||||||
completion?(.failure(error))
|
completion?(.failure(error))
|
||||||
|
|
||||||
case let .success(account, _):
|
case let .success(account, _):
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.account = account
|
self.account = account
|
||||||
}
|
|
||||||
self.persistentContainer.backgroundContext.perform {
|
self.persistentContainer.backgroundContext.perform {
|
||||||
if let accountMO = self.persistentContainer.account(for: account.id, in: self.persistentContainer.backgroundContext) {
|
if let accountMO = self.persistentContainer.account(for: account.id, in: self.persistentContainer.backgroundContext) {
|
||||||
accountMO.updateFrom(apiAccount: account, container: self.persistentContainer)
|
accountMO.updateFrom(apiAccount: account, container: self.persistentContainer)
|
||||||
|
@ -120,12 +118,13 @@ class MastodonController: ObservableObject {
|
||||||
let request = Client.getInstance()
|
let request = Client.getInstance()
|
||||||
run(request) { (response) in
|
run(request) { (response) in
|
||||||
guard case let .success(instance, _) = response else { fatalError() }
|
guard case let .success(instance, _) = response else { fatalError() }
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
completion?(instance)
|
completion?(instance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ObservableObject so that SwiftUI views can receive it through @EnvironmentObject
|
||||||
|
extension MastodonController: ObservableObject {}
|
||||||
|
|
|
@ -158,11 +158,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
mastodonController.getOwnInstance()
|
mastodonController.getOwnInstance()
|
||||||
|
|
||||||
let rootController: UIViewController
|
let rootController: UIViewController
|
||||||
|
#if SDK_IOS_14
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
rootController = MainSplitViewController(mastodonController: mastodonController)
|
rootController = MainSplitViewController(mastodonController: mastodonController)
|
||||||
} else {
|
} else {
|
||||||
rootController = MainTabBarViewController(mastodonController: mastodonController)
|
rootController = MainTabBarViewController(mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
rootController = MainTabBarViewController(mastodonController: mastodonController)
|
||||||
|
#endif
|
||||||
window!.rootViewController = rootController
|
window!.rootViewController = rootController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,9 +91,7 @@ struct ComposeAttachmentsList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var canAddAttachment: Bool {
|
private var canAddAttachment: Bool {
|
||||||
switch mastodonController.instance?.instanceType {
|
switch mastodonController.instance.instanceType {
|
||||||
case nil:
|
|
||||||
return false
|
|
||||||
case .pleroma:
|
case .pleroma:
|
||||||
return true
|
return true
|
||||||
case .mastodon:
|
case .mastodon:
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ComposeAvatarImageView: View {
|
struct ComposeAvatarImageView: View {
|
||||||
let url: URL?
|
let url: URL
|
||||||
@State var request: ImageCache.Request? = nil
|
@State var request: ImageCache.Request? = nil
|
||||||
@State var avatarImage: UIImage? = nil
|
@State var avatarImage: UIImage? = nil
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
@ -19,9 +19,7 @@ struct ComposeAvatarImageView: View {
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
||||||
.conditionally(url != nil) {
|
.onAppear(perform: self.loadImage)
|
||||||
$0.onAppear(perform: self.loadImage)
|
|
||||||
}
|
|
||||||
.onDisappear(perform: self.cancelRequest)
|
.onDisappear(perform: self.cancelRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +27,6 @@ struct ComposeAvatarImageView: View {
|
||||||
if let avatarImage = avatarImage {
|
if let avatarImage = avatarImage {
|
||||||
return Image(uiImage: avatarImage)
|
return Image(uiImage: avatarImage)
|
||||||
} else {
|
} else {
|
||||||
return placeholderImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var placeholderImage: Image {
|
|
||||||
let imageName: String
|
let imageName: String
|
||||||
switch preferences.avatarStyle {
|
switch preferences.avatarStyle {
|
||||||
case .circle:
|
case .circle:
|
||||||
|
@ -43,19 +36,15 @@ struct ComposeAvatarImageView: View {
|
||||||
}
|
}
|
||||||
return Image(systemName: imageName)
|
return Image(systemName: imageName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
guard let url = url else { return }
|
|
||||||
request = ImageCache.avatars.get(url) { (data) in
|
request = ImageCache.avatars.get(url) { (data) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.request = nil
|
||||||
if let data = data, let image = UIImage(data: data) {
|
if let data = data, let image = UIImage(data: data) {
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.request = nil
|
|
||||||
self.avatarImage = image
|
self.avatarImage = image
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.request = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,19 +12,17 @@ import Pachyderm
|
||||||
struct ComposeCurrentAccount: View {
|
struct ComposeCurrentAccount: View {
|
||||||
@EnvironmentObject var mastodonController: MastodonController
|
@EnvironmentObject var mastodonController: MastodonController
|
||||||
|
|
||||||
var account: Account? {
|
var account: Account {
|
||||||
mastodonController.account
|
mastodonController.account!
|
||||||
}
|
}
|
||||||
|
|
||||||
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 != nil ? "\(account!.displayName) avatar" : "Avatar"))
|
.accessibility(label: Text("\(account.displayName) avatar"))
|
||||||
|
|
||||||
if let id = account?.id,
|
|
||||||
let account = mastodonController.persistentContainer.account(for: id) {
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
AccountDisplayNameLabel(account: account, fontSize: 20)
|
AccountDisplayNameLabel(account: mastodonController.persistentContainer.account(for: account.id)!, fontSize: 20)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
Text(verbatim: "@\(account.acct)")
|
Text(verbatim: "@\(account.acct)")
|
||||||
|
@ -33,9 +31,6 @@ struct ComposeCurrentAccount: View {
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,11 +58,15 @@ class ComposeDrawingViewController: UIViewController {
|
||||||
canvasView.drawing = initialDrawing
|
canvasView.drawing = initialDrawing
|
||||||
}
|
}
|
||||||
canvasView.delegate = self
|
canvasView.delegate = self
|
||||||
|
#if SDK_IOS_14
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
canvasView.drawingPolicy = .anyInput
|
canvasView.drawingPolicy = .anyInput
|
||||||
} else {
|
} else {
|
||||||
canvasView.allowsFingerDrawing = true
|
canvasView.allowsFingerDrawing = true
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
canvasView.allowsFingerDrawing = true
|
||||||
|
#endif
|
||||||
canvasView.minimumZoomScale = 0.5
|
canvasView.minimumZoomScale = 0.5
|
||||||
canvasView.maximumZoomScale = 2
|
canvasView.maximumZoomScale = 2
|
||||||
canvasView.backgroundColor = .systemBackground
|
canvasView.backgroundColor = .systemBackground
|
||||||
|
|
|
@ -27,7 +27,13 @@ struct ComposeTextView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
Color(backgroundColor)
|
WrappedTextView(
|
||||||
|
text: $text,
|
||||||
|
textDidChange: self.textDidChange,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
font: .systemFont(ofSize: fontSize)
|
||||||
|
)
|
||||||
|
.frame(height: height ?? minHeight)
|
||||||
|
|
||||||
if text.isEmpty, let placeholder = placeholder {
|
if text.isEmpty, let placeholder = placeholder {
|
||||||
placeholder
|
placeholder
|
||||||
|
@ -35,13 +41,6 @@ struct ComposeTextView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.offset(x: 4, y: 8)
|
.offset(x: 4, y: 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedTextView(
|
|
||||||
text: $text,
|
|
||||||
textDidChange: self.textDidChange,
|
|
||||||
font: .systemFont(ofSize: fontSize)
|
|
||||||
)
|
|
||||||
.frame(height: height ?? minHeight)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,13 +73,14 @@ struct WrappedTextView: UIViewRepresentable {
|
||||||
|
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
var textDidChange: ((UITextView) -> Void)?
|
var textDidChange: ((UITextView) -> Void)?
|
||||||
|
var backgroundColor = UIColor.secondarySystemBackground
|
||||||
var font = UIFont.systemFont(ofSize: 20)
|
var font = UIFont.systemFont(ofSize: 20)
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UITextView {
|
func makeUIView(context: Context) -> UITextView {
|
||||||
let textView = UITextView()
|
let textView = UITextView()
|
||||||
textView.delegate = context.coordinator
|
textView.delegate = context.coordinator
|
||||||
textView.isEditable = true
|
textView.isEditable = true
|
||||||
textView.backgroundColor = .clear
|
textView.backgroundColor = backgroundColor
|
||||||
textView.font = font
|
textView.font = font
|
||||||
textView.textContainer.lineBreakMode = .byWordWrapping
|
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||||
return textView
|
return textView
|
||||||
|
|
|
@ -28,7 +28,7 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var charactersRemaining: Int {
|
var charactersRemaining: Int {
|
||||||
let limit = mastodonController.instance?.maxStatusCharacters ?? 500
|
let limit = mastodonController.instance.maxStatusCharacters ?? 500
|
||||||
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
|
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
|
||||||
return limit - (cwCount + CharacterCounter.count(text: draft.text))
|
return limit - (cwCount + CharacterCounter.count(text: draft.text))
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,15 +20,6 @@ struct MainComposeTextView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
Color(UIColor.secondarySystemBackground)
|
|
||||||
|
|
||||||
if draft.text.isEmpty {
|
|
||||||
placeholder
|
|
||||||
.font(.system(size: 20))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.offset(x: 4, y: 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
MainComposeWrappedTextView(
|
MainComposeWrappedTextView(
|
||||||
text: $draft.text,
|
text: $draft.text,
|
||||||
visibility: draft.visibility,
|
visibility: draft.visibility,
|
||||||
|
@ -37,6 +28,13 @@ struct MainComposeTextView: View {
|
||||||
self.height = max(textView.contentSize.height, minHeight)
|
self.height = max(textView.contentSize.height, minHeight)
|
||||||
}
|
}
|
||||||
.frame(height: height ?? minHeight)
|
.frame(height: height ?? minHeight)
|
||||||
|
|
||||||
|
if draft.text.isEmpty {
|
||||||
|
placeholder
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.offset(x: 4, y: 8)
|
||||||
|
}
|
||||||
}.onAppear {
|
}.onAppear {
|
||||||
if !hasFirstAppeared {
|
if !hasFirstAppeared {
|
||||||
hasFirstAppeared = true
|
hasFirstAppeared = true
|
||||||
|
@ -61,7 +59,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
let textView = UITextView()
|
let textView = UITextView()
|
||||||
textView.delegate = context.coordinator
|
textView.delegate = context.coordinator
|
||||||
textView.isEditable = true
|
textView.isEditable = true
|
||||||
textView.backgroundColor = .clear
|
textView.backgroundColor = .secondarySystemBackground
|
||||||
textView.font = .systemFont(ofSize: 20)
|
textView.font = .systemFont(ofSize: 20)
|
||||||
textView.textContainer.lineBreakMode = .byWordWrapping
|
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||||
context.coordinator.textView = textView
|
context.coordinator.textView = textView
|
||||||
|
|
|
@ -52,8 +52,6 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
title = NSLocalizedString("Conversation", comment: "conversation screen title")
|
|
||||||
|
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
tableView.dataSource = self
|
tableView.dataSource = self
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
||||||
super.init()
|
super.init()
|
||||||
self.viewController = viewController
|
self.viewController = viewController
|
||||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
||||||
|
if #available(iOS 13.4, *) {
|
||||||
panRecognizer.allowedScrollTypesMask = .all
|
panRecognizer.allowedScrollTypesMask = .all
|
||||||
|
}
|
||||||
viewController.view.addGestureRecognizer(panRecognizer)
|
viewController.view.addGestureRecognizer(panRecognizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
|
#if SDK_IOS_14
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
protocol MainSidebarViewControllerDelegate: class {
|
protocol MainSidebarViewControllerDelegate: class {
|
||||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||||
|
@ -379,3 +380,4 @@ extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
#if SDK_IOS_14
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
|
@ -315,15 +316,11 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(tab: MainTabBarViewController.Tab) {
|
func select(tab: MainTabBarViewController.Tab) {
|
||||||
if traitCollection.horizontalSizeClass == .compact {
|
|
||||||
tabBarViewController?.select(tab: tab)
|
|
||||||
} else {
|
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
presentCompose()
|
presentCompose()
|
||||||
} else {
|
} else {
|
||||||
select(item: .tab(tab))
|
select(item: .tab(tab))
|
||||||
sidebar.select(item: .tab(tab), animated: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
|
@ -36,10 +36,12 @@ class InteractivePushTransition: UIPercentDrivenInteractiveTransition {
|
||||||
interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
||||||
navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer)
|
navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer)
|
||||||
|
|
||||||
|
if #available(iOS 13.4, *) {
|
||||||
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
|
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
|
||||||
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
||||||
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
|
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) {
|
@objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) {
|
||||||
interactive = true
|
interactive = true
|
||||||
|
|
|
@ -54,6 +54,8 @@ extension MenuPreviewProvider {
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// todo: handle pre-iOS 14
|
||||||
|
#if SDK_IOS_14
|
||||||
if accountID != mastodonController.account.id,
|
if accountID != mastodonController.account.id,
|
||||||
#available(iOS 14.0, *) {
|
#available(iOS 14.0, *) {
|
||||||
actionsSection.append(UIDeferredMenuElement({ (elementHandler) in
|
actionsSection.append(UIDeferredMenuElement({ (elementHandler) in
|
||||||
|
@ -69,7 +71,7 @@ extension MenuPreviewProvider {
|
||||||
let following = relationship.following
|
let following = relationship.following
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
elementHandler([
|
elementHandler([
|
||||||
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
|
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.minus", handler: { (_) in
|
||||||
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
||||||
mastodonController.run(request) { (_) in
|
mastodonController.run(request) { (_) in
|
||||||
}
|
}
|
||||||
|
@ -80,6 +82,7 @@ extension MenuPreviewProvider {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
let shareSection = [
|
let shareSection = [
|
||||||
openInSafariAction(url: account.url),
|
openInSafariAction(url: account.url),
|
||||||
|
|
|
@ -47,10 +47,9 @@ enum AppShortcutItem: String, CaseIterable {
|
||||||
}
|
}
|
||||||
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
|
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
|
||||||
let window = scene.windows.first { $0.isKeyWindow }!
|
let window = scene.windows.first { $0.isKeyWindow }!
|
||||||
if let controller = window.rootViewController as? TuskerRootViewController {
|
let controller = window.rootViewController as! MainTabBarViewController
|
||||||
controller.select(tab: tab)
|
controller.select(tab: tab)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppShortcutItem {
|
extension AppShortcutItem {
|
||||||
|
|
|
@ -58,11 +58,11 @@ class AttachmentsContainerView: UIView {
|
||||||
attachmentViews.removeAllObjects()
|
attachmentViews.removeAllObjects()
|
||||||
moreView?.removeFromSuperview()
|
moreView?.removeFromSuperview()
|
||||||
|
|
||||||
var accessibilityElements = [Any]()
|
|
||||||
|
|
||||||
if attachments.count > 0 {
|
if attachments.count > 0 {
|
||||||
self.isHidden = false
|
self.isHidden = false
|
||||||
|
|
||||||
|
var accessibilityElements = [Any]()
|
||||||
|
|
||||||
switch attachments.count {
|
switch attachments.count {
|
||||||
case 1:
|
case 1:
|
||||||
let attachmentView = createAttachmentView(index: 0, hSize: .full, vSize: .full)
|
let attachmentView = createAttachmentView(index: 0, hSize: .full, vSize: .full)
|
||||||
|
@ -215,16 +215,13 @@ class AttachmentsContainerView: UIView {
|
||||||
accessibilityElements.append(topRight)
|
accessibilityElements.append(topRight)
|
||||||
accessibilityElements.append(bottomLeft)
|
accessibilityElements.append(bottomLeft)
|
||||||
accessibilityElements.append(moreView)
|
accessibilityElements.append(moreView)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.accessibilityElements = accessibilityElements
|
||||||
} else {
|
} else {
|
||||||
self.isHidden = true
|
self.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure accessibilityElements is set every time the UI is updated, otherwise it holds
|
|
||||||
// on to strong references to the old set of attachment views
|
|
||||||
self.accessibilityElements = accessibilityElements
|
|
||||||
|
|
||||||
contentHidden = Preferences.shared.blurAllMedia || status.sensitive
|
contentHidden = Preferences.shared.blurAllMedia || status.sensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusReplyView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/6/19.
|
||||||
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class ComposeStatusReplyView: UIView {
|
||||||
|
|
||||||
|
weak var mastodonController: MastodonController?
|
||||||
|
|
||||||
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
|
@IBOutlet weak var statusContentTextView: StatusContentTextView!
|
||||||
|
|
||||||
|
var avatarRequest: ImageCache.Request?
|
||||||
|
|
||||||
|
static func create() -> ComposeStatusReplyView {
|
||||||
|
return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
avatarRequest?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
updateUIForPreferences()
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateUIForPreferences() {
|
||||||
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(for status: StatusMO) {
|
||||||
|
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||||
|
usernameLabel.text = "@\(status.account.acct)"
|
||||||
|
statusContentTextView.overrideMastodonController = mastodonController
|
||||||
|
statusContentTextView.setTextFrom(status: status)
|
||||||
|
|
||||||
|
avatarRequest = ImageCache.avatars.get(status.account.avatar) { [weak self] (data) in
|
||||||
|
guard let self = self, let data = data else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarImageView.image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ComposeStatusReplyView" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Ypn-Ed-MTq">
|
||||||
|
<rect key="frame" x="8" y="8" width="50" height="50"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="50" id="8qi-gl-5ci"/>
|
||||||
|
<constraint firstAttribute="width" constant="50" id="Dy2-jh-AJj"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2cE-sS-Uut">
|
||||||
|
<rect key="frame" x="66" y="8" width="301" height="651"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Sdv-dB-Plm" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0yZ-71-eTj">
|
||||||
|
<rect key="frame" x="115" y="0.0" width="178" height="21"/>
|
||||||
|
<accessibility key="accessibilityConfiguration">
|
||||||
|
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
||||||
|
<bool key="isElement" value="NO"/>
|
||||||
|
</accessibility>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="atN-ay-ceL" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="25" width="301" height="626"/>
|
||||||
|
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||||
|
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
|
</textView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="atN-ay-ceL" secondAttribute="bottom" id="3ub-qq-laF"/>
|
||||||
|
<constraint firstItem="Sdv-dB-Plm" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="6v5-7p-9gm"/>
|
||||||
|
<constraint firstItem="Sdv-dB-Plm" firstAttribute="top" secondItem="2cE-sS-Uut" secondAttribute="top" id="YmP-yU-sfe"/>
|
||||||
|
<constraint firstItem="0yZ-71-eTj" firstAttribute="top" secondItem="2cE-sS-Uut" secondAttribute="top" id="bdX-ge-bMT"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="0yZ-71-eTj" secondAttribute="trailing" constant="8" id="hU7-aZ-ibI"/>
|
||||||
|
<constraint firstItem="atN-ay-ceL" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="k5c-jg-Dy8"/>
|
||||||
|
<constraint firstItem="0yZ-71-eTj" firstAttribute="leading" secondItem="Sdv-dB-Plm" secondAttribute="trailing" constant="8" id="m0X-YU-m3V"/>
|
||||||
|
<constraint firstItem="atN-ay-ceL" firstAttribute="top" secondItem="0yZ-71-eTj" secondAttribute="bottom" constant="4" id="pXc-4g-PAe"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="atN-ay-ceL" secondAttribute="trailing" id="qcg-bA-8ba"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="2cE-sS-Uut" firstAttribute="height" relation="greaterThanOrEqual" secondItem="Ypn-Ed-MTq" secondAttribute="height" id="Fn3-o4-RGx"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="2cE-sS-Uut" secondAttribute="bottom" constant="8" id="G2d-Kz-c4e"/>
|
||||||
|
<constraint firstItem="Ypn-Ed-MTq" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="8" id="MbW-9d-3gC"/>
|
||||||
|
<constraint firstItem="2cE-sS-Uut" firstAttribute="leading" secondItem="Ypn-Ed-MTq" secondAttribute="trailing" constant="8" id="TS2-Sr-PB3"/>
|
||||||
|
<constraint firstItem="2cE-sS-Uut" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" id="cat-Cr-PSV"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="2cE-sS-Uut" secondAttribute="trailing" constant="8" id="eH4-lG-5UR"/>
|
||||||
|
<constraint firstItem="Ypn-Ed-MTq" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" placeholder="YES" id="xCn-8G-jUZ"/>
|
||||||
|
</constraints>
|
||||||
|
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="avatarImageView" destination="Ypn-Ed-MTq" id="eea-bc-klc"/>
|
||||||
|
<outlet property="displayNameLabel" destination="Sdv-dB-Plm" id="RxW-Ra-Ups"/>
|
||||||
|
<outlet property="statusContentTextView" destination="atN-ay-ceL" id="i6A-Rd-rJp"/>
|
||||||
|
<outlet property="usernameLabel" destination="0yZ-71-eTj" id="VQm-Dq-3zP"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="138.40000000000001" y="-72.863568215892059"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
</document>
|
|
@ -67,11 +67,15 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
|
if #available(iOS 13.4, *) {
|
||||||
moreButton.addInteraction(UIPointerInteraction(delegate: self))
|
moreButton.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
|
}
|
||||||
|
#if SDK_IOS_14
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
moreButton.showsMenuAsPrimaryAction = true
|
moreButton.showsMenuAsPrimaryAction = true
|
||||||
moreButton.isContextMenuInteractionEnabled = true
|
moreButton.isContextMenuInteractionEnabled = true
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(for accountID: String) {
|
func updateUI(for accountID: String) {
|
||||||
|
|
|
@ -87,9 +87,11 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
||||||
attachmentsView.isAccessibilityElement = true
|
attachmentsView.isAccessibilityElement = true
|
||||||
|
|
||||||
|
#if SDK_IOS_14
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
moreButton.showsMenuAsPrimaryAction = true
|
moreButton.showsMenuAsPrimaryAction = true
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
@ -120,25 +122,19 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final func updateUI(statusID: String, state: StatusState) {
|
func updateUI(statusID: String, state: StatusState) {
|
||||||
createObserversIfNecessary()
|
createObserversIfNecessary()
|
||||||
|
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
fatalError("Missing cached status")
|
fatalError("Missing cached status")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.statusID = statusID
|
self.statusID = statusID
|
||||||
|
|
||||||
doUpdateUI(status: status, state: state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func doUpdateUI(status: StatusMO, state: StatusState) {
|
|
||||||
self.statusState = state
|
self.statusState = state
|
||||||
|
|
||||||
let account = status.account
|
let account = status.account
|
||||||
self.accountID = account.id
|
self.accountID = account.id
|
||||||
updateUI(account: account)
|
updateUI(account: account)
|
||||||
updateUIForPreferences(account: account, status: status)
|
updateUIForPreferences(account: account)
|
||||||
|
|
||||||
attachmentsView.updateUI(status: status)
|
attachmentsView.updateUI(status: status)
|
||||||
attachmentsView.isAccessibilityElement = status.attachments.count > 0
|
attachmentsView.isAccessibilityElement = status.attachments.count > 0
|
||||||
|
@ -198,10 +194,12 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if SDK_IOS_14
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(statusID: statusID, sourceView: moreButton))
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(statusID: statusID, sourceView: moreButton))
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
|
@ -215,19 +213,18 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc func preferencesChanged() {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID),
|
let account = mastodonController.persistentContainer.account(for: accountID),
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) else { return }
|
let status = mastodonController.persistentContainer.status(for: statusID) else { return }
|
||||||
updateUIForPreferences(account: account, status: status)
|
updateUIForPreferences(account: account)
|
||||||
|
updateStatusIconsForPreferences(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIForPreferences(account: AccountMO, status: StatusMO) {
|
func updateUIForPreferences(account: AccountMO) {
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
|
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
|
||||||
|
|
||||||
updateStatusIconsForPreferences(status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateStatusIconsForPreferences(_ status: StatusMO) {
|
func updateStatusIconsForPreferences(_ status: StatusMO) {
|
||||||
|
|
|
@ -38,8 +38,9 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func doUpdateUI(status: StatusMO, state: StatusState) {
|
override func updateUI(statusID: String, state: StatusState) {
|
||||||
super.doUpdateUI(status: status, state: state)
|
super.updateUI(statusID: statusID, state: state)
|
||||||
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError() }
|
||||||
|
|
||||||
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
|
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
|
||||||
if let application = status.applicationName {
|
if let application = status.applicationName {
|
||||||
|
@ -62,8 +63,8 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji
|
profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
|
override func updateUIForPreferences(account: AccountMO) {
|
||||||
super.updateUIForPreferences(account: account, status: status)
|
super.updateUIForPreferences(account: account)
|
||||||
|
|
||||||
favoriteAndReblogCountStackView.isHidden = !Preferences.shared.showFavoriteAndReblogCounts
|
favoriteAndReblogCountStackView.isHidden = !Preferences.shared.showFavoriteAndReblogCounts
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,9 +68,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func doUpdateUI(status: StatusMO, state: StatusState) {
|
override func updateUI(statusID: String, state: StatusState) {
|
||||||
var status = status
|
guard var status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||||
|
|
||||||
|
let realStatusID: String
|
||||||
if let rebloggedStatus = status.reblog {
|
if let rebloggedStatus = status.reblog {
|
||||||
reblogStatusID = statusID
|
reblogStatusID = statusID
|
||||||
rebloggerID = status.account.id
|
rebloggerID = status.account.id
|
||||||
|
@ -78,24 +79,25 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
updateRebloggerLabel(reblogger: status.account)
|
updateRebloggerLabel(reblogger: status.account)
|
||||||
|
|
||||||
status = rebloggedStatus
|
status = rebloggedStatus
|
||||||
statusID = rebloggedStatus.id
|
realStatusID = rebloggedStatus.id
|
||||||
} else {
|
} else {
|
||||||
reblogStatusID = nil
|
reblogStatusID = nil
|
||||||
rebloggerID = nil
|
rebloggerID = nil
|
||||||
reblogLabel.isHidden = true
|
reblogLabel.isHidden = true
|
||||||
|
realStatusID = statusID
|
||||||
}
|
}
|
||||||
|
|
||||||
super.doUpdateUI(status: status, state: state)
|
super.updateUI(statusID: realStatusID, state: state)
|
||||||
|
|
||||||
doUpdateTimestamp(status: status)
|
updateTimestamp()
|
||||||
|
|
||||||
let pinned = showPinned && (status.pinned ?? false)
|
let pinned = showPinned && (status.pinned ?? false)
|
||||||
timestampLabel.isHidden = pinned
|
timestampLabel.isHidden = pinned
|
||||||
pinImageView.isHidden = !pinned
|
pinImageView.isHidden = !pinned
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
|
@objc override func preferencesChanged() {
|
||||||
super.updateUIForPreferences(account: account, status: status)
|
super.preferencesChanged()
|
||||||
|
|
||||||
if let rebloggerID = rebloggerID,
|
if let rebloggerID = rebloggerID,
|
||||||
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||||
|
@ -119,16 +121,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
|
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTimestamp() {
|
func updateTimestamp() {
|
||||||
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
|
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
|
||||||
// so we bail out immediately, since there's nothing to update
|
// so we bail out immediately, since there's nothing to update
|
||||||
guard let mastodonController = mastodonController else { return }
|
guard let mastodonController = mastodonController else { return }
|
||||||
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!)") }
|
||||||
|
|
||||||
doUpdateTimestamp(status: status)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func doUpdateTimestamp(status: StatusMO) {
|
|
||||||
timestampLabel.text = status.createdAt.timeAgoString()
|
timestampLabel.text = status.createdAt.timeAgoString()
|
||||||
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
|
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,9 @@ class VisualEffectImageButton: UIControl {
|
||||||
imageView.bottomAnchor.constraint(equalTo: vibrancyView.bottomAnchor, constant: -2),
|
imageView.bottomAnchor.constraint(equalTo: vibrancyView.bottomAnchor, constant: -2),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
#if SDK_IOS_14
|
||||||
addInteraction(UIContextMenuInteraction(delegate: self))
|
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
|
#endif
|
||||||
|
|
||||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
|
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
|
||||||
}
|
}
|
||||||
|
@ -61,10 +63,12 @@ class VisualEffectImageButton: UIControl {
|
||||||
sendActions(for: .touchUpInside)
|
sendActions(for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if SDK_IOS_14
|
||||||
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
guard let menu = menu else { return nil }
|
guard let menu = menu else { return nil }
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,16 +16,12 @@ fileprivate class WeakWrapper<T: AnyObject> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollection {
|
struct WeakArray<Element: AnyObject>: Collection {
|
||||||
private var array: [WeakWrapper<Element>]
|
private var array: [WeakWrapper<Element>]
|
||||||
|
|
||||||
var startIndex: Int { array.startIndex }
|
var startIndex: Int { array.startIndex }
|
||||||
var endIndex: Int { array.endIndex }
|
var endIndex: Int { array.endIndex }
|
||||||
|
|
||||||
init() {
|
|
||||||
array = []
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ elements: [Element]) {
|
init(_ elements: [Element]) {
|
||||||
array = elements.map { WeakWrapper($0) }
|
array = elements.map { WeakWrapper($0) }
|
||||||
}
|
}
|
||||||
|
@ -34,20 +30,11 @@ struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollect
|
||||||
array = elements.map { WeakWrapper($0) }
|
array = elements.map { WeakWrapper($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript(position: Int) -> Element? {
|
subscript(_ index: Int) -> Element? {
|
||||||
get {
|
return array[index].value
|
||||||
array[position].value
|
|
||||||
}
|
|
||||||
set(newValue) {
|
|
||||||
array[position] = WeakWrapper(newValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func index(after i: Int) -> Int {
|
func index(after i: Int) -> Int {
|
||||||
return array.index(after: i)
|
return array.index(after: i)
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Self.Element == C.Element {
|
|
||||||
array.replaceSubrange(subrange, with: newElements.map { WeakWrapper($0) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue