Compare commits
33 Commits
b61418e062
...
4ce8de280e
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 4ce8de280e | |
Shadowfacts | 4018d39312 | |
Shadowfacts | ae416bb604 | |
Shadowfacts | 5e9caf9179 | |
Shadowfacts | 3bbbb05083 | |
Shadowfacts | bd3e74c611 | |
Shadowfacts | 2e8c416e04 | |
Shadowfacts | 955f9e5916 | |
Shadowfacts | 17f15db32d | |
Shadowfacts | 1a11dd2a69 | |
Shadowfacts | b5fa0bceab | |
Shadowfacts | c224d11417 | |
Shadowfacts | bebf47f05c | |
Shadowfacts | e76b719c6a | |
Shadowfacts | 478c7b7a23 | |
Shadowfacts | e3cc0df283 | |
Shadowfacts | 9ed05de3ee | |
Shadowfacts | 64f41ea2b7 | |
Shadowfacts | 9af4118dfc | |
Shadowfacts | 64a8f6d733 | |
Shadowfacts | ca76568c79 | |
Shadowfacts | 18e91feb00 | |
Shadowfacts | c5d2e9af68 | |
Shadowfacts | 0691c3b9d6 | |
Shadowfacts | 1ccb450477 | |
Shadowfacts | 7117ce6320 | |
Shadowfacts | 34dccf1f37 | |
Shadowfacts | a3303dc8fb | |
Shadowfacts | d15fa2199e | |
Shadowfacts | fadddeda7f | |
Shadowfacts | b232bec80f | |
Shadowfacts | 1b19a13b05 | |
Shadowfacts | cd5b4c1145 |
|
@ -29,15 +29,15 @@ public class Attachment: Decodable {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
self.kind = try container.decode(Kind.self, forKey: .kind)
|
self.kind = try container.decode(Kind.self, forKey: .kind)
|
||||||
self.url = URL(lenient: try container.decode(String.self, forKey: .url))!
|
self.url = URL(string: try container.decode(String.self, forKey: .url))!
|
||||||
|
self.previewURL = URL(string: try container.decode(String.self, forKey: .previewURL))!
|
||||||
if let remote = try? container.decode(String.self, forKey: .remoteURL) {
|
if let remote = try? container.decode(String.self, forKey: .remoteURL) {
|
||||||
self.remoteURL = URL(lenient: remote.replacingOccurrences(of: " ", with: "%20"))
|
self.remoteURL = URL(string: remote)!
|
||||||
} else {
|
} else {
|
||||||
self.remoteURL = nil
|
self.remoteURL = nil
|
||||||
}
|
}
|
||||||
self.previewURL = URL(lenient: try container.decode(String.self, forKey: .previewURL).replacingOccurrences(of: " ", with: "%20"))!
|
|
||||||
if let text = try? container.decode(String.self, forKey: .textURL) {
|
if let text = try? container.decode(String.self, forKey: .textURL) {
|
||||||
self.textURL = URL(lenient: text.replacingOccurrences(of: " ", with: "%20"))
|
self.textURL = URL(string: text)!
|
||||||
} else {
|
} else {
|
||||||
self.textURL = nil
|
self.textURL = nil
|
||||||
}
|
}
|
||||||
|
@ -113,14 +113,3 @@ extension Attachment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension URL {
|
|
||||||
private static let allowedChars = CharacterSet.urlHostAllowed.union(.urlPathAllowed).union(.urlQueryAllowed)
|
|
||||||
|
|
||||||
init?(lenient string: String) {
|
|
||||||
guard let escaped = string.addingPercentEncoding(withAllowedCharacters: URL.allowedChars) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.init(string: escaped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -22,6 +22,23 @@ public class Card: Decodable {
|
||||||
public let width: Int?
|
public let width: Int?
|
||||||
public let height: Int?
|
public let height: Int?
|
||||||
|
|
||||||
|
public required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
self.url = try container.decode(URL.self, forKey: .url)
|
||||||
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
|
self.description = try container.decode(String.self, forKey: .description)
|
||||||
|
self.kind = try container.decode(Kind.self, forKey: .kind)
|
||||||
|
self.image = try? container.decode(URL.self, forKey: .image)
|
||||||
|
self.authorName = try? container.decode(String.self, forKey: .authorName)
|
||||||
|
self.authorURL = try? container.decode(URL.self, forKey: .authorURL)
|
||||||
|
self.providerName = try? container.decode(String.self, forKey: .providerName)
|
||||||
|
self.providerURL = try? container.decode(URL.self, forKey: .providerURL)
|
||||||
|
self.html = try? container.decode(String.self, forKey: .html)
|
||||||
|
self.width = try? container.decode(Int.self, forKey: .width)
|
||||||
|
self.height = try? container.decode(Int.self, forKey: .height)
|
||||||
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case url
|
case url
|
||||||
case title
|
case title
|
||||||
|
|
|
@ -36,6 +36,7 @@ public class Status: Decodable {
|
||||||
public let language: String?
|
public let language: String?
|
||||||
public let pinned: Bool?
|
public let pinned: Bool?
|
||||||
public let bookmarked: Bool?
|
public let bookmarked: Bool?
|
||||||
|
public let card: Card?
|
||||||
|
|
||||||
public static func getContext(_ status: Status) -> Request<ConversationContext> {
|
public static func getContext(_ status: Status) -> Request<ConversationContext> {
|
||||||
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(status.id)/context")
|
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(status.id)/context")
|
||||||
|
@ -128,6 +129,7 @@ public class Status: Decodable {
|
||||||
case language
|
case language
|
||||||
case pinned
|
case pinned
|
||||||
case bookmarked
|
case bookmarked
|
||||||
|
case card
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,11 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041160FE22B442870030A9B7 /* AttachmentViewController.swift */; };
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */; };
|
||||||
0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 041160FF22B442870030A9B7 /* AttachmentViewController.xib */; };
|
|
||||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
|
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
||||||
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */; };
|
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */; };
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
||||||
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */; };
|
|
||||||
0454DDB122B467AA00B8BB8E /* GalleryShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDB022B467AA00B8BB8E /* GalleryShrinkAnimationController.swift */; };
|
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
||||||
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0461A38F2163CBAE00C0A807 /* Cache.framework */; };
|
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0461A38F2163CBAE00C0A807 /* Cache.framework */; };
|
||||||
|
@ -24,7 +21,7 @@
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||||
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; };
|
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; };
|
||||||
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */; };
|
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; };
|
||||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
|
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
|
||||||
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
|
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
|
||||||
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; };
|
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; };
|
||||||
|
@ -76,7 +73,7 @@
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||||
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
|
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
|
||||||
D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachment.swift */; };
|
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; };
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; };
|
||||||
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; };
|
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; };
|
||||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493A23C1000300612E6E /* AlbumTableViewCell.swift */; };
|
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493A23C1000300612E6E /* AlbumTableViewCell.swift */; };
|
||||||
|
@ -105,10 +102,14 @@
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
||||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
||||||
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; };
|
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
|
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
||||||
|
D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */; };
|
||||||
|
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */; };
|
||||||
|
D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */; };
|
||||||
|
D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */; };
|
||||||
|
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
||||||
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
|
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
||||||
|
@ -287,14 +288,11 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
041160FE22B442870030A9B7 /* AttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewController.swift; sourceTree = "<group>"; };
|
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingLargeImageViewController.swift; sourceTree = "<group>"; };
|
||||||
041160FF22B442870030A9B7 /* AttachmentViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AttachmentViewController.xib; sourceTree = "<group>"; };
|
|
||||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = "<group>"; };
|
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = "<group>"; };
|
||||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
||||||
0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentActionPrefs.swift; sourceTree = "<group>"; };
|
0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentActionPrefs.swift; sourceTree = "<group>"; };
|
||||||
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
||||||
0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryExpandAnimationController.swift; sourceTree = "<group>"; };
|
|
||||||
0454DDB022B467AA00B8BB8E /* GalleryShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryShrinkAnimationController.swift; sourceTree = "<group>"; };
|
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
||||||
0461A38F2163CBAE00C0A807 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
0461A38F2163CBAE00C0A807 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -303,8 +301,8 @@
|
||||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
||||||
D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; };
|
D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; };
|
||||||
|
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentsViewController.swift; sourceTree = "<group>"; };
|
||||||
D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; };
|
D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; };
|
||||||
D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeMediaView.xib; sourceTree = "<group>"; };
|
|
||||||
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
|
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = "<group>"; };
|
D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = "<group>"; };
|
||||||
|
@ -357,7 +355,7 @@
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = "<group>"; };
|
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||||
D626493423BD94CE00612E6E /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = "<group>"; };
|
||||||
D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = "<group>"; };
|
D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AllPhotosTableViewCell.xib; sourceTree = "<group>"; };
|
D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AllPhotosTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D626493A23C1000300612E6E /* AlbumTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumTableViewCell.swift; sourceTree = "<group>"; };
|
D626493A23C1000300612E6E /* AlbumTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -386,9 +384,13 @@
|
||||||
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
||||||
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
||||||
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
||||||
D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = "<group>"; };
|
|
||||||
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
||||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
|
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
|
||||||
|
D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddAttachmentTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeAttachmentTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||||
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
||||||
|
@ -564,40 +566,12 @@
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
0411610422B4571E0030A9B7 /* Attachment */ = {
|
0411610522B457290030A9B7 /* Attachment Gallery */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
041160FE22B442870030A9B7 /* AttachmentViewController.swift */,
|
|
||||||
041160FF22B442870030A9B7 /* AttachmentViewController.xib */,
|
|
||||||
);
|
|
||||||
path = Attachment;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
0411610522B457290030A9B7 /* Gallery */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
0411610622B457360030A9B7 /* Transitions */,
|
|
||||||
04D14BAE22B34A2800642648 /* GalleryViewController.swift */,
|
04D14BAE22B34A2800642648 /* GalleryViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Gallery;
|
path = "Attachment Gallery";
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
0411610622B457360030A9B7 /* Transitions */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */,
|
|
||||||
0454DDB022B467AA00B8BB8E /* GalleryShrinkAnimationController.swift */,
|
|
||||||
);
|
|
||||||
path = Transitions;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D60C07E221E817560057FAA8 /* Compose Media */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */,
|
|
||||||
D6333B762138D94E00CE884A /* ComposeMediaView.swift */,
|
|
||||||
);
|
|
||||||
path = "Compose Media";
|
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D61099AC2144B0CC00432DC2 /* Pachyderm */ = {
|
D61099AC2144B0CC00432DC2 /* Pachyderm */ = {
|
||||||
|
@ -695,6 +669,37 @@
|
||||||
path = "Hashtag Cell";
|
path = "Hashtag Cell";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D61959D0241E842400A37B8E /* Draft Cell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */,
|
||||||
|
D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = "Draft Cell";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D61959D1241E844900A37B8E /* Attachment Cells */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */,
|
||||||
|
D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */,
|
||||||
|
D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */,
|
||||||
|
D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */,
|
||||||
|
);
|
||||||
|
path = "Attachment Cells";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D61959D2241E846D00A37B8E /* Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
|
||||||
|
D620483123D2A6A3008A63EF /* CompositionState.swift */,
|
||||||
|
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */,
|
||||||
|
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */,
|
||||||
|
);
|
||||||
|
path = Models;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D61AC1DA232EA43100C54D2D /* Instance Cell */ = {
|
D61AC1DA232EA43100C54D2D /* Instance Cell */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -762,8 +767,6 @@
|
||||||
children = (
|
children = (
|
||||||
D627FF78217E950100CC0648 /* DraftsTableViewController.xib */,
|
D627FF78217E950100CC0648 /* DraftsTableViewController.xib */,
|
||||||
D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */,
|
D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */,
|
||||||
D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */,
|
|
||||||
D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */,
|
|
||||||
);
|
);
|
||||||
path = Drafts;
|
path = Drafts;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -790,12 +793,13 @@
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
D641C786213DD852004B4513 /* Notifications */,
|
D641C786213DD852004B4513 /* Notifications */,
|
||||||
D641C787213DD862004B4513 /* Compose */,
|
D641C787213DD862004B4513 /* Compose */,
|
||||||
|
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||||
|
D627FF77217E94F200CC0648 /* Drafts */,
|
||||||
D627943C23A5635D00D38C68 /* Explore */,
|
D627943C23A5635D00D38C68 /* Explore */,
|
||||||
D6BC9DD8232D8BCA002CA326 /* Search */,
|
D6BC9DD8232D8BCA002CA326 /* Search */,
|
||||||
D627944B23A9A02400D38C68 /* Lists */,
|
D627944B23A9A02400D38C68 /* Lists */,
|
||||||
D641C788213DD86D004B4513 /* Large Image */,
|
D641C788213DD86D004B4513 /* Large Image */,
|
||||||
0411610422B4571E0030A9B7 /* Attachment */,
|
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||||
0411610522B457290030A9B7 /* Gallery */,
|
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||||
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
||||||
D627944823A6AD5100D38C68 /* Bookmarks */,
|
D627944823A6AD5100D38C68 /* Bookmarks */,
|
||||||
|
@ -861,13 +865,9 @@
|
||||||
D641C787213DD862004B4513 /* Compose */ = {
|
D641C787213DD862004B4513 /* Compose */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
|
||||||
D627FF77217E94F200CC0648 /* Drafts */,
|
|
||||||
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */,
|
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */,
|
||||||
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
|
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
|
||||||
D626493423BD94CE00612E6E /* CompositionAttachment.swift */,
|
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */,
|
||||||
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
|
|
||||||
D620483123D2A6A3008A63EF /* CompositionState.swift */,
|
|
||||||
);
|
);
|
||||||
path = Compose;
|
path = Compose;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -878,6 +878,7 @@
|
||||||
D646C954213B364600269FB5 /* Transitions */,
|
D646C954213B364600269FB5 /* Transitions */,
|
||||||
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */,
|
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */,
|
||||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
|
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
|
||||||
|
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */,
|
||||||
);
|
);
|
||||||
path = "Large Image";
|
path = "Large Image";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1129,8 +1130,9 @@
|
||||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||||
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
||||||
D60C07E221E817560057FAA8 /* Compose Media */,
|
|
||||||
D626494023C122C800612E6E /* Asset Picker */,
|
D626494023C122C800612E6E /* Asset Picker */,
|
||||||
|
D61959D1241E844900A37B8E /* Attachment Cells */,
|
||||||
|
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||||
D641C78A213DD926004B4513 /* Status */,
|
D641C78A213DD926004B4513 /* Status */,
|
||||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||||
|
@ -1211,6 +1213,7 @@
|
||||||
D663626021360A9600C9CBA2 /* Preferences */,
|
D663626021360A9600C9CBA2 /* Preferences */,
|
||||||
D6AEBB3F2321640F00E5038B /* Activities */,
|
D6AEBB3F2321640F00E5038B /* Activities */,
|
||||||
D667E5F62135C2ED0057A976 /* Extensions */,
|
D667E5F62135C2ED0057A976 /* Extensions */,
|
||||||
|
D61959D2241E846D00A37B8E /* Models */,
|
||||||
D6F953F121251A2F00CF0F2B /* Controllers */,
|
D6F953F121251A2F00CF0F2B /* Controllers */,
|
||||||
D641C780213DD7C4004B4513 /* Screens */,
|
D641C780213DD7C4004B4513 /* Screens */,
|
||||||
D6BED1722126661300F02DA0 /* Views */,
|
D6BED1722126661300F02DA0 /* Views */,
|
||||||
|
@ -1479,9 +1482,9 @@
|
||||||
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 */,
|
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */,
|
||||||
0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */,
|
D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */,
|
||||||
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
||||||
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */,
|
D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */,
|
||||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||||
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
|
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
|
||||||
);
|
);
|
||||||
|
@ -1589,7 +1592,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */,
|
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */,
|
||||||
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
|
@ -1601,7 +1604,6 @@
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||||
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
|
|
||||||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
|
@ -1612,7 +1614,7 @@
|
||||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||||
|
@ -1626,6 +1628,7 @@
|
||||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||||
|
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */,
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||||
|
@ -1634,7 +1637,6 @@
|
||||||
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */,
|
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */,
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
||||||
0454DDB122B467AA00B8BB8E /* GalleryShrinkAnimationController.swift in Sources */,
|
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
|
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
|
||||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||||
|
@ -1642,11 +1644,11 @@
|
||||||
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
||||||
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
||||||
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
||||||
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
|
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||||
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
||||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||||
|
D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */,
|
||||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||||
|
@ -1656,6 +1658,7 @@
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
||||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||||
|
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */,
|
||||||
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
|
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
|
||||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
|
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
||||||
|
@ -1684,6 +1687,7 @@
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
||||||
|
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */,
|
||||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
|
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
|
||||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
|
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||||
|
@ -2014,7 +2018,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
@ -2039,7 +2043,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
|
|
@ -88,11 +88,16 @@ class MastodonController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnInstance() {
|
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
||||||
|
if let instance = self.instance {
|
||||||
|
completion?(instance)
|
||||||
|
} else {
|
||||||
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() }
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
|
completion?(instance)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ class DraftsManager: Codable {
|
||||||
return drafts.sorted(by: { $0.lastModified > $1.lastModified })
|
return drafts.sorted(by: { $0.lastModified > $1.lastModified })
|
||||||
}
|
}
|
||||||
|
|
||||||
func create(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft {
|
func create(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [CompositionAttachment]) -> Draft {
|
||||||
let draft = Draft(accountID: accountID, text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments)
|
let draft = Draft(accountID: accountID, text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments)
|
||||||
drafts.append(draft)
|
drafts.append(draft)
|
||||||
return draft
|
return draft
|
||||||
|
@ -58,11 +58,11 @@ extension DraftsManager {
|
||||||
private(set) var accountID: String
|
private(set) var accountID: String
|
||||||
private(set) var text: String
|
private(set) var text: String
|
||||||
private(set) var contentWarning: String?
|
private(set) var contentWarning: String?
|
||||||
private(set) var attachments: [DraftAttachment]
|
var attachments: [CompositionAttachment]
|
||||||
private(set) var inReplyToID: String?
|
private(set) var inReplyToID: String?
|
||||||
private(set) var lastModified: Date
|
private(set) var lastModified: Date
|
||||||
|
|
||||||
init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) {
|
init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [CompositionAttachment], lastModified: Date = Date()) {
|
||||||
self.id = UUID()
|
self.id = UUID()
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
self.text = text
|
self.text = text
|
||||||
|
@ -72,7 +72,7 @@ extension DraftsManager {
|
||||||
self.lastModified = lastModified
|
self.lastModified = lastModified
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(accountID: String, text: String, contentWarning: String?, attachments: [DraftAttachment]) {
|
func update(accountID: String, text: String, contentWarning: String?, attachments: [CompositionAttachment]) {
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
self.text = text
|
self.text = text
|
||||||
self.contentWarning = contentWarning
|
self.contentWarning = contentWarning
|
||||||
|
@ -84,9 +84,4 @@ extension DraftsManager {
|
||||||
return lhs.id == rhs.id
|
return lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DraftAttachment: Codable {
|
|
||||||
let attachment: CompositionAttachment
|
|
||||||
let description: String
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,23 +10,17 @@ import UIKit
|
||||||
|
|
||||||
extension UIViewController: UIViewControllerTransitioningDelegate {
|
extension UIViewController: UIViewControllerTransitioningDelegate {
|
||||||
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
if let presented = presented as? LargeImageViewController,
|
if let presented = presented as? LargeImageAnimatableViewController,
|
||||||
presented.sourceInfo?.image != nil {
|
presented.animationImage != nil {
|
||||||
return LargeImageExpandAnimationController()
|
return LargeImageExpandAnimationController()
|
||||||
} else if let presented = presented as? GalleryViewController,
|
|
||||||
presented.sourcesInfo[presented.startIndex]?.image != nil {
|
|
||||||
return GalleryExpandAnimationController()
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
if let dismissed = dismissed as? LargeImageViewController,
|
if let dismissed = dismissed as? LargeImageAnimatableViewController,
|
||||||
dismissed.imageForDismissalAnimation() != nil {
|
dismissed.animationImage != nil {
|
||||||
return LargeImageShrinkAnimationController(interactionController: dismissed.dismissInteractionController)
|
return LargeImageShrinkAnimationController(interactionController: dismissed.dismissInteractionController)
|
||||||
} else if let dismissed = dismissed as? GalleryViewController,
|
|
||||||
dismissed.imageForDismissalAnimation() != nil {
|
|
||||||
return GalleryShrinkAnimationController(interactionController: dismissed.dismissInteractionController)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -36,10 +30,6 @@ extension UIViewController: UIViewControllerTransitioningDelegate {
|
||||||
let interactionController = animator.interactionController,
|
let interactionController = animator.interactionController,
|
||||||
interactionController.inProgress {
|
interactionController.inProgress {
|
||||||
return interactionController
|
return interactionController
|
||||||
} else if let animator = animator as? GalleryShrinkAnimationController,
|
|
||||||
let interactionController = animator.interactionController,
|
|
||||||
interactionController.inProgress {
|
|
||||||
return interactionController
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,5 +98,16 @@
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array/>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>space.vaccor.Tusker.composition-attachment</string>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
//
|
||||||
|
// CompositionAttachment.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/14/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import MobileCoreServices
|
||||||
|
|
||||||
|
final class CompositionAttachment: NSObject, Codable {
|
||||||
|
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
||||||
|
|
||||||
|
let data: CompositionAttachmentData
|
||||||
|
var attachmentDescription: String
|
||||||
|
|
||||||
|
init(data: CompositionAttachmentData, description: String = "") {
|
||||||
|
self.data = data
|
||||||
|
self.attachmentDescription = description
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: CompositionAttachment, rhs: CompositionAttachment) -> Bool {
|
||||||
|
return lhs.data == rhs.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let imageType = kUTTypeImage as String
|
||||||
|
private let mp4Type = kUTTypeMPEG4 as String
|
||||||
|
private let quickTimeType = kUTTypeQuickTimeMovie as String
|
||||||
|
private let dataType = kUTTypeData as String
|
||||||
|
|
||||||
|
extension CompositionAttachment: NSItemProviderWriting {
|
||||||
|
static var writableTypeIdentifiersForItemProvider: [String] {
|
||||||
|
[typeIdentifier]
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
|
||||||
|
if typeIdentifier == CompositionAttachment.typeIdentifier {
|
||||||
|
do {
|
||||||
|
completionHandler(try PropertyListEncoder().encode(self), nil)
|
||||||
|
} catch {
|
||||||
|
completionHandler(nil, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(nil, ItemProviderError.incompatibleTypeIdentifier)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ItemProviderError: Error {
|
||||||
|
case incompatibleTypeIdentifier
|
||||||
|
|
||||||
|
var localizedDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .incompatibleTypeIdentifier:
|
||||||
|
return "Cannot provide data for given type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CompositionAttachment: NSItemProviderReading {
|
||||||
|
static var readableTypeIdentifiersForItemProvider: [String] {
|
||||||
|
// todo: is there a better way of handling movies than manually adding all possible UTI types?
|
||||||
|
// just using kUTTypeMovie doesn't work, because we need the actually type in order to get the file extension
|
||||||
|
// without the file extension, getting the thumbnail and exporting the video for attachment upload fails
|
||||||
|
[typeIdentifier] + UIImage.readableTypeIdentifiersForItemProvider + [mp4Type, quickTimeType] + NSURL.readableTypeIdentifiersForItemProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
|
||||||
|
if typeIdentifier == CompositionAttachment.typeIdentifier {
|
||||||
|
return try PropertyListDecoder().decode(Self.self, from: data)
|
||||||
|
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
|
||||||
|
return CompositionAttachment(data: .image(image)) as! Self
|
||||||
|
} else if typeIdentifier == mp4Type || typeIdentifier == quickTimeType {
|
||||||
|
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
|
let temporaryFileName = ProcessInfo().globallyUniqueString
|
||||||
|
let fileExt = UTTypeCopyPreferredTagWithClass(typeIdentifier as CFString, kUTTagClassFilenameExtension)!
|
||||||
|
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt.takeUnretainedValue() as String)
|
||||||
|
try data.write(to: temporaryFileURL)
|
||||||
|
return CompositionAttachment(data: .video(temporaryFileURL)) as! Self
|
||||||
|
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {
|
||||||
|
return CompositionAttachment(data: .video(url)) as! Self
|
||||||
|
} else {
|
||||||
|
throw ItemProviderError.incompatibleTypeIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
//
|
||||||
|
// CompositionAttachmentData.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/1/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Photos
|
||||||
|
import MobileCoreServices
|
||||||
|
|
||||||
|
enum CompositionAttachmentData {
|
||||||
|
case asset(PHAsset)
|
||||||
|
case image(UIImage)
|
||||||
|
case video(URL)
|
||||||
|
|
||||||
|
var type: AttachmentType {
|
||||||
|
switch self {
|
||||||
|
case let .asset(asset):
|
||||||
|
return asset.attachmentType!
|
||||||
|
case .image(_):
|
||||||
|
return .image
|
||||||
|
case .video(_):
|
||||||
|
return .video
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isAsset: Bool {
|
||||||
|
switch self {
|
||||||
|
case .asset(_):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var canSaveToDraft: Bool {
|
||||||
|
switch self {
|
||||||
|
case .video(_):
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getData(completion: @escaping (Data, String) -> Void) {
|
||||||
|
switch self {
|
||||||
|
case let .image(image):
|
||||||
|
completion(image.pngData()!, "image/png")
|
||||||
|
case let .asset(asset):
|
||||||
|
if asset.mediaType == .image {
|
||||||
|
let options = PHImageRequestOptions()
|
||||||
|
options.version = .current
|
||||||
|
options.deliveryMode = .highQualityFormat
|
||||||
|
options.resizeMode = .none
|
||||||
|
options.isNetworkAccessAllowed = true
|
||||||
|
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in
|
||||||
|
guard var data = data, let dataUTI = dataUTI else { fatalError() }
|
||||||
|
|
||||||
|
let mimeType: String
|
||||||
|
if dataUTI == "public.heic" {
|
||||||
|
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
|
||||||
|
let image = CIImage(data: data)!
|
||||||
|
let context = CIContext()
|
||||||
|
let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
|
||||||
|
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
} else {
|
||||||
|
mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(data, mimeType)
|
||||||
|
}
|
||||||
|
} else if asset.mediaType == .video {
|
||||||
|
let options = PHVideoRequestOptions()
|
||||||
|
options.deliveryMode = .automatic
|
||||||
|
options.isNetworkAccessAllowed = true
|
||||||
|
options.version = .current
|
||||||
|
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
||||||
|
guard let exportSession = exportSession else { fatalError("failed to create export session") }
|
||||||
|
CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fatalError("assetType must be either image or video")
|
||||||
|
}
|
||||||
|
case let .video(url):
|
||||||
|
let asset = AVURLAsset(url: url)
|
||||||
|
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
||||||
|
fatalError("failed to create export session")
|
||||||
|
}
|
||||||
|
CompositionAttachmentData.exportVideoData(session: session, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) {
|
||||||
|
session.outputFileType = .mp4
|
||||||
|
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
||||||
|
session.exportAsynchronously {
|
||||||
|
guard session.status == .completed else { fatalError("video export failed: \(String(describing: session.error))") }
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: session.outputURL!)
|
||||||
|
completion(data, "video/mp4")
|
||||||
|
} catch {
|
||||||
|
fatalError("Unable to load video: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AttachmentType {
|
||||||
|
case image, video
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PHAsset {
|
||||||
|
var attachmentType: CompositionAttachmentData.AttachmentType? {
|
||||||
|
switch self.mediaType {
|
||||||
|
case .image:
|
||||||
|
return .image
|
||||||
|
case .video:
|
||||||
|
return .video
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CompositionAttachmentData: Codable {
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
switch self {
|
||||||
|
case let .asset(asset):
|
||||||
|
try container.encode("asset", forKey: .type)
|
||||||
|
try container.encode(asset.localIdentifier, forKey: .assetIdentifier)
|
||||||
|
case let .image(image):
|
||||||
|
try container.encode("image", forKey: .type)
|
||||||
|
try container.encode(image.pngData()!, forKey: .imageData)
|
||||||
|
case .video(_):
|
||||||
|
throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "video CompositionAttachments cannot be encoded"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
switch try container.decode(String.self, forKey: .type) {
|
||||||
|
case "asset":
|
||||||
|
let identifier = try container.decode(String.self, forKey: .assetIdentifier)
|
||||||
|
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .assetIdentifier, in: container, debugDescription: "Could not fetch asset with local identifier")
|
||||||
|
}
|
||||||
|
self = .asset(asset)
|
||||||
|
case "image":
|
||||||
|
guard let image = UIImage(data: try container.decode(Data.self, forKey: .imageData)) else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "Could not decode UIImage from image data")
|
||||||
|
}
|
||||||
|
self = .image(image)
|
||||||
|
default:
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of 'image' or 'asset'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case type
|
||||||
|
case imageData
|
||||||
|
/// The local identifier of the PHAsset for this attachment
|
||||||
|
case assetIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CompositionAttachmentData: Equatable {
|
||||||
|
static func ==(lhs: CompositionAttachmentData, rhs: CompositionAttachmentData) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case let (.asset(a), .asset(b)):
|
||||||
|
return a.localIdentifier == b.localIdentifier
|
||||||
|
case let (.image(a), .image(b)):
|
||||||
|
return a == b
|
||||||
|
case let (.video(a), .video(b)):
|
||||||
|
return a == b
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,6 +82,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||||
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||||
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
|
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||||
|
|
||||||
|
Preferences.save()
|
||||||
|
DraftsManager.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
|
@ -92,6 +95,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
func sceneWillResignActive(_ scene: UIScene) {
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
// Called when the scene will move from an active state to an inactive state.
|
// Called when the scene will move from an active state to an inactive state.
|
||||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||||
|
|
||||||
|
Preferences.save()
|
||||||
|
DraftsManager.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||||
|
@ -103,9 +109,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// Called as the scene transitions from the foreground to the background.
|
// Called as the scene transitions from the foreground to the background.
|
||||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
// to restore the scene back to its current state.
|
// to restore the scene back to its current state.
|
||||||
|
|
||||||
Preferences.save()
|
|
||||||
DraftsManager.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func activateAccount(_ account: LocalData.UserAccountInfo) {
|
func activateAccount(_ account: LocalData.UserAccountInfo) {
|
||||||
|
|
|
@ -49,6 +49,21 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// use the safe area layout guide instead of letting it automatically use the safe area insets
|
||||||
|
// because otherwise, when presented in a popover with the arrow on the left or right side,
|
||||||
|
// the collection view content will be cut off by the width of the arrow because the popover
|
||||||
|
// doesn't respect safe area insets
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
|
||||||
|
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
|
||||||
|
// top ignores safe area because when presented in the sheet container, it simplifies the top content offset
|
||||||
|
view.topAnchor.constraint(equalTo: collectionView.topAnchor),
|
||||||
|
// bottom ignores safe area because we want cells to underflow bottom of the screen on notched iPhones
|
||||||
|
view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
|
||||||
|
])
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
||||||
|
|
||||||
collectionView.alwaysBounceVertical = true
|
collectionView.alwaysBounceVertical = true
|
||||||
|
@ -170,12 +185,23 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
let asset = fetchResult.object(at: indexPath.row - 1)
|
let asset = fetchResult.object(at: indexPath.row - 1)
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
|
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
|
||||||
return AssetPreviewViewController(asset: asset)
|
return AssetPreviewViewController(asset: asset)
|
||||||
}, actionProvider: nil)
|
}, actionProvider: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||||
|
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
|
||||||
|
let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell {
|
||||||
|
let parameters = UIPreviewParameters()
|
||||||
|
parameters.backgroundColor = .black
|
||||||
|
return UITargetedPreview(view: cell.imageView, parameters: parameters)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func donePressed() {
|
@objc func donePressed() {
|
|
@ -33,6 +33,8 @@ class AssetPickerSheetContainerViewController: SheetContainerViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
assetPicker.view.layer.cornerRadius = view.bounds.width * 0.02
|
assetPicker.view.layer.cornerRadius = view.bounds.width * 0.02
|
||||||
|
// don't round bottom corners, since they'll always be cut off by the device
|
||||||
|
assetPicker.view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
}
|
}
|
|
@ -10,15 +10,15 @@ import UIKit
|
||||||
import Photos
|
import Photos
|
||||||
|
|
||||||
protocol AssetPickerViewControllerDelegate {
|
protocol AssetPickerViewControllerDelegate {
|
||||||
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool
|
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool
|
||||||
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment])
|
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData])
|
||||||
}
|
}
|
||||||
|
|
||||||
class AssetPickerViewController: UINavigationController {
|
class AssetPickerViewController: UINavigationController {
|
||||||
|
|
||||||
var assetPickerDelegate: AssetPickerViewControllerDelegate?
|
var assetPickerDelegate: AssetPickerViewControllerDelegate?
|
||||||
|
|
||||||
var currentCollectionSelectedAssets: [CompositionAttachment] {
|
var currentCollectionSelectedAssets: [CompositionAttachmentData] {
|
||||||
if let vc = visibleViewController as? AssetCollectionViewController {
|
if let vc = visibleViewController as? AssetCollectionViewController {
|
||||||
return vc.selectedAssets.map { .asset($0) }
|
return vc.selectedAssets.map { .asset($0) }
|
||||||
} else {
|
} else {
|
||||||
|
@ -70,7 +70,7 @@ extension AssetPickerViewController: AssetCollectionViewControllerDelegate {
|
||||||
|
|
||||||
extension AssetPickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
extension AssetPickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||||
let attachment: CompositionAttachment
|
let attachment: CompositionAttachmentData
|
||||||
if let image = info[.originalImage] as? UIImage {
|
if let image = info[.originalImage] as? UIImage {
|
||||||
attachment = .image(image)
|
attachment = .image(image)
|
||||||
} else if let url = info[.mediaURL] as? URL {
|
} else if let url = info[.mediaURL] as? URL {
|
|
@ -13,14 +13,18 @@ import AVKit
|
||||||
|
|
||||||
class AssetPreviewViewController: UIViewController {
|
class AssetPreviewViewController: UIViewController {
|
||||||
|
|
||||||
let asset: PHAsset
|
let attachment: CompositionAttachmentData
|
||||||
|
|
||||||
init(asset: PHAsset) {
|
init(attachment: CompositionAttachmentData) {
|
||||||
self.asset = asset
|
self.attachment = attachment
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convenience init(asset: PHAsset) {
|
||||||
|
self.init(attachment: .asset(asset))
|
||||||
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
@ -30,21 +34,29 @@ class AssetPreviewViewController: UIViewController {
|
||||||
|
|
||||||
view.backgroundColor = .black
|
view.backgroundColor = .black
|
||||||
|
|
||||||
if asset.mediaType == .image {
|
switch attachment {
|
||||||
|
case let .image(image):
|
||||||
|
showImage(image)
|
||||||
|
case let .video(url):
|
||||||
|
showVideo(asset: AVURLAsset(url: url))
|
||||||
|
case let .asset(asset):
|
||||||
|
switch asset.mediaType {
|
||||||
|
case .image:
|
||||||
if asset.mediaSubtypes.contains(.photoLive) {
|
if asset.mediaSubtypes.contains(.photoLive) {
|
||||||
showLivePhoto()
|
showLivePhoto(asset)
|
||||||
} else {
|
} else {
|
||||||
showImage()
|
showAssetImage(asset)
|
||||||
}
|
}
|
||||||
} else if asset.mediaType == .video {
|
case .video:
|
||||||
playVideo()
|
showAssetVideo(asset)
|
||||||
} else {
|
default:
|
||||||
fatalError("asset mediaType must be image or video")
|
fatalError("asset mediaType must be image or video")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func showImage() {
|
func showImage(_ image: UIImage) {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView(image: image)
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.contentMode = .scaleAspectFit
|
imageView.contentMode = .scaleAspectFit
|
||||||
view.addSubview(imageView)
|
view.addSubview(imageView)
|
||||||
|
@ -54,7 +66,10 @@ class AssetPreviewViewController: UIViewController {
|
||||||
imageView.topAnchor.constraint(equalTo: view.topAnchor),
|
imageView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
preferredContentSize = image.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAssetImage(_ asset: PHAsset) {
|
||||||
let options = PHImageRequestOptions()
|
let options = PHImageRequestOptions()
|
||||||
options.version = .current
|
options.version = .current
|
||||||
options.deliveryMode = .opportunistic
|
options.deliveryMode = .opportunistic
|
||||||
|
@ -62,12 +77,12 @@ class AssetPreviewViewController: UIViewController {
|
||||||
options.isNetworkAccessAllowed = true
|
options.isNetworkAccessAllowed = true
|
||||||
PHImageManager.default().requestImage(for: asset, targetSize: view.bounds.size, contentMode: .aspectFit, options: options) { (image, _) in
|
PHImageManager.default().requestImage(for: asset, targetSize: view.bounds.size, contentMode: .aspectFit, options: options) { (image, _) in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
imageView.image = image
|
self.showImage(image!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showLivePhoto() {
|
func showLivePhoto(_ asset: PHAsset) {
|
||||||
let options = PHLivePhotoRequestOptions()
|
let options = PHLivePhotoRequestOptions()
|
||||||
options.deliveryMode = .opportunistic
|
options.deliveryMode = .opportunistic
|
||||||
options.version = .current
|
options.version = .current
|
||||||
|
@ -90,11 +105,23 @@ class AssetPreviewViewController: UIViewController {
|
||||||
livePhotoView.topAnchor.constraint(equalTo: self.view.topAnchor),
|
livePhotoView.topAnchor.constraint(equalTo: self.view.topAnchor),
|
||||||
livePhotoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
|
livePhotoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
self.preferredContentSize = livePhoto.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func playVideo() {
|
func showVideo(asset: AVAsset) {
|
||||||
|
let playerController = AVPlayerViewController()
|
||||||
|
let item = AVPlayerItem(asset: asset)
|
||||||
|
let player = AVPlayer(playerItem: item)
|
||||||
|
player.isMuted = true
|
||||||
|
player.play()
|
||||||
|
playerController.player = player
|
||||||
|
self.embedChild(playerController)
|
||||||
|
self.preferredContentSize = item.presentationSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAssetVideo(_ asset: PHAsset) {
|
||||||
let options = PHVideoRequestOptions()
|
let options = PHVideoRequestOptions()
|
||||||
options.deliveryMode = .automatic
|
options.deliveryMode = .automatic
|
||||||
options.isNetworkAccessAllowed = true
|
options.isNetworkAccessAllowed = true
|
||||||
|
@ -104,13 +131,7 @@ class AssetPreviewViewController: UIViewController {
|
||||||
fatalError("failed to get AVAsset")
|
fatalError("failed to get AVAsset")
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let playerController = AVPlayerViewController()
|
self.showVideo(asset: avAsset)
|
||||||
let item = AVPlayerItem(asset: avAsset)
|
|
||||||
let player = AVPlayer(playerItem: item)
|
|
||||||
player.isMuted = true
|
|
||||||
player.play()
|
|
||||||
playerController.player = player
|
|
||||||
self.embedChild(playerController)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,14 +8,16 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class AttachmentViewController: UIViewController {
|
class PendingLargeImageViewController: UIViewController {
|
||||||
|
|
||||||
let attachment: Attachment
|
let url: URL
|
||||||
|
let cache: ImageCache
|
||||||
|
let imageDescription: String?
|
||||||
|
|
||||||
var largeImageVC: LargeImageViewController?
|
var largeImageVC: LargeImageViewController?
|
||||||
var loadingVC: LoadingViewController?
|
var loadingVC: LoadingViewController?
|
||||||
|
|
||||||
var attachmentRequest: ImageCache.Request?
|
var imageRequest: ImageCache.Request?
|
||||||
|
|
||||||
private var initialControlsVisible: Bool = true
|
private var initialControlsVisible: Bool = true
|
||||||
var controlsVisible: Bool {
|
var controlsVisible: Bool {
|
||||||
|
@ -35,10 +37,16 @@ class AttachmentViewController: UIViewController {
|
||||||
return largeImageVC
|
return largeImageVC
|
||||||
}
|
}
|
||||||
|
|
||||||
init(attachment: Attachment) {
|
init(url: URL, cache: ImageCache, imageDescription: String?) {
|
||||||
self.attachment = attachment
|
self.url = url
|
||||||
|
self.cache = cache
|
||||||
|
self.imageDescription = imageDescription
|
||||||
|
|
||||||
super.init(nibName: "AttachmentViewController", bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(attachment: Attachment) {
|
||||||
|
self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -51,14 +59,14 @@ class AttachmentViewController: UIViewController {
|
||||||
overrideUserInterfaceStyle = .dark
|
overrideUserInterfaceStyle = .dark
|
||||||
view.backgroundColor = .black
|
view.backgroundColor = .black
|
||||||
|
|
||||||
if let data = ImageCache.attachments.get(attachment.url) {
|
if let data = cache.get(url) {
|
||||||
createLargeImage(data: data)
|
createLargeImage(data: data)
|
||||||
} else {
|
} else {
|
||||||
loadingVC = LoadingViewController()
|
loadingVC = LoadingViewController()
|
||||||
embedChild(loadingVC!)
|
embedChild(loadingVC!)
|
||||||
attachmentRequest = ImageCache.attachments.get(attachment.url) { [weak self] (data) in
|
imageRequest = cache.get(url) { [weak self] (data) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.attachmentRequest = nil
|
self.imageRequest = nil
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingVC?.removeViewAndController()
|
self.loadingVC?.removeViewAndController()
|
||||||
self.createLargeImage(data: data!)
|
self.createLargeImage(data: data!)
|
||||||
|
@ -71,16 +79,16 @@ class AttachmentViewController: UIViewController {
|
||||||
super.didMove(toParent: parent)
|
super.didMove(toParent: parent)
|
||||||
|
|
||||||
if parent == nil {
|
if parent == nil {
|
||||||
attachmentRequest?.cancel()
|
imageRequest?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createLargeImage(data: Data) {
|
func createLargeImage(data: Data) {
|
||||||
guard let image = UIImage(data: data) else { return }
|
guard let image = UIImage(data: data) else { return }
|
||||||
largeImageVC = LargeImageViewController(image: image, description: attachment.description, sourceInfo: nil)
|
largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceInfo: nil)
|
||||||
largeImageVC!.initialControlsVisible = initialControlsVisible
|
largeImageVC!.initialControlsVisible = initialControlsVisible
|
||||||
largeImageVC!.shrinkGestureEnabled = false
|
largeImageVC!.shrinkGestureEnabled = false
|
||||||
if attachment.url.pathExtension == "gif" {
|
if url.pathExtension == "gif" {
|
||||||
largeImageVC!.gifData = data
|
largeImageVC!.gifData = data
|
||||||
}
|
}
|
||||||
embedChild(largeImageVC!)
|
embedChild(largeImageVC!)
|
|
@ -10,9 +10,7 @@ import Pachyderm
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import AVKit
|
import AVKit
|
||||||
|
|
||||||
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
|
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController {
|
||||||
|
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
|
||||||
|
|
||||||
let attachments: [Attachment]
|
let attachments: [Attachment]
|
||||||
let sourcesInfo: [LargeImageViewController.SourceInfo?]
|
let sourcesInfo: [LargeImageViewController.SourceInfo?]
|
||||||
|
@ -28,6 +26,24 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var animationSourceInfo: LargeImageViewController.SourceInfo? { sourcesInfo[currentIndex] }
|
||||||
|
var animationImage: UIImage? {
|
||||||
|
if let sourceImage = sourcesInfo[currentIndex]?.image {
|
||||||
|
return sourceImage
|
||||||
|
} else {
|
||||||
|
return (pages[currentIndex] as? LoadingLargeImageViewController)?.largeImageVC?.image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var animationGifData: Data? {
|
||||||
|
let attachment = attachments[currentIndex]
|
||||||
|
if attachment.url.pathExtension == "gif" {
|
||||||
|
return ImageCache.attachments.get(attachment.url)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
override var prefersStatusBarHidden: Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -51,7 +67,9 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
self.pages = attachments.map {
|
self.pages = attachments.map {
|
||||||
switch $0.kind {
|
switch $0.kind {
|
||||||
case .image:
|
case .image:
|
||||||
return AttachmentViewController(attachment: $0)
|
let vc = LoadingLargeImageViewController(attachment: $0)
|
||||||
|
vc.shrinkGestureEnabled = false
|
||||||
|
return vc
|
||||||
case .video, .audio:
|
case .video, .audio:
|
||||||
let vc = AVPlayerViewController()
|
let vc = AVPlayerViewController()
|
||||||
vc.player = AVPlayer(url: $0.url)
|
vc.player = AVPlayer(url: $0.url)
|
||||||
|
@ -97,14 +115,6 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageForDismissalAnimation() -> UIImage? {
|
|
||||||
if let sourceImage = sourcesInfo[currentIndex]?.image {
|
|
||||||
return sourceImage
|
|
||||||
} else {
|
|
||||||
return (pages[currentIndex] as? AttachmentViewController)?.largeImageVC?.image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Page View Controller Data Source
|
// MARK: - Page View Controller Data Source
|
||||||
|
|
||||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||||
|
@ -125,8 +135,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
|
|
||||||
// MARK: - Page View Controller Delegate
|
// MARK: - Page View Controller Delegate
|
||||||
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||||
if let pending = pendingViewControllers.first as? AttachmentViewController,
|
if let pending = pendingViewControllers.first as? LoadingLargeImageViewController,
|
||||||
let current = viewControllers!.first as? AttachmentViewController {
|
let current = viewControllers!.first as? LoadingLargeImageViewController {
|
||||||
pending.controlsVisible = current.controlsVisible
|
pending.controlsVisible = current.controlsVisible
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14810.11" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14766.13"/>
|
|
||||||
<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" customClass="AttachmentViewController" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<connections>
|
|
||||||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
|
||||||
</connections>
|
|
||||||
</placeholder>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
|
||||||
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
|
||||||
<point key="canvasLocation" x="139" y="3"/>
|
|
||||||
</view>
|
|
||||||
</objects>
|
|
||||||
</document>
|
|
|
@ -0,0 +1,454 @@
|
||||||
|
//
|
||||||
|
// ComposeAttachmentsViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/11/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import MobileCoreServices
|
||||||
|
|
||||||
|
protocol ComposeAttachmentsViewControllerDelegate: class {
|
||||||
|
func composeSelectedAttachmentsDidChange()
|
||||||
|
func composeRequiresAttachmentDescriptionsDidChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComposeAttachmentsViewController: UITableViewController {
|
||||||
|
|
||||||
|
weak var mastodonController: MastodonController!
|
||||||
|
weak var delegate: ComposeAttachmentsViewControllerDelegate?
|
||||||
|
|
||||||
|
private var heightConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
var attachments: [CompositionAttachment] = [] {
|
||||||
|
didSet {
|
||||||
|
delegate?.composeSelectedAttachmentsDidChange()
|
||||||
|
delegate?.composeRequiresAttachmentDescriptionsDidChange()
|
||||||
|
updateAddAttachmentsButtonEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiresAttachmentDescriptions: Bool {
|
||||||
|
if Preferences.shared.requireAttachmentDescriptions {
|
||||||
|
return attachments.contains { $0.attachmentDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(attachments: [CompositionAttachment], mastodonController: MastodonController) {
|
||||||
|
self.attachments = attachments
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
super.init(style: .plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 96
|
||||||
|
|
||||||
|
tableView.register(UINib(nibName: "AddAttachmentTableViewCell", bundle: .main), forCellReuseIdentifier: "addAttachment")
|
||||||
|
tableView.register(UINib(nibName: "ComposeAttachmentTableViewCell", bundle: .main), forCellReuseIdentifier: "composeAttachment")
|
||||||
|
|
||||||
|
// you would think the table view could handle this itself, but no, using a constraint on the table view's contentLayoutGuide doesn't work
|
||||||
|
// add extra space, so when dropping items, the add attachment cell doesn't disappear
|
||||||
|
heightConstraint = tableView.heightAnchor.constraint(equalToConstant: tableView.contentSize.height + 80)
|
||||||
|
heightConstraint.isActive = true
|
||||||
|
|
||||||
|
// prevents extra separator lines from appearing when the height of the table view is greater than the height of the content
|
||||||
|
tableView.tableFooterView = UIView()
|
||||||
|
|
||||||
|
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
||||||
|
|
||||||
|
// enable dragging on iPhone to allow reordering
|
||||||
|
tableView.dragInteractionEnabled = true
|
||||||
|
tableView.dragDelegate = self
|
||||||
|
tableView.dropDelegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
updateHeightConstraint()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setAttachments(_ attachments: [CompositionAttachment]) {
|
||||||
|
tableView.performBatchUpdates({
|
||||||
|
tableView.deleteRows(at: self.attachments.indices.map { IndexPath(row: $0, section: 0) }, with: .automatic)
|
||||||
|
self.attachments = attachments
|
||||||
|
tableView.insertRows(at: self.attachments.indices.map { IndexPath(row: $0, section: 0) }, with: .automatic)
|
||||||
|
})
|
||||||
|
updateHeightConstraint()
|
||||||
|
delegate?.composeRequiresAttachmentDescriptionsDidChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHeightConstraint() {
|
||||||
|
// add extra space, so when dropping items, the add attachment cell doesn't disappear
|
||||||
|
heightConstraint.constant = tableView.contentSize.height + 80
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isAddAttachmentsButtonEnabled() -> Bool {
|
||||||
|
switch mastodonController.instance.instanceType {
|
||||||
|
case .pleroma:
|
||||||
|
return true
|
||||||
|
case .mastodon:
|
||||||
|
return !attachments.contains(where: { $0.data.type == .video }) && attachments.count < 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAddAttachmentsButtonEnabled() {
|
||||||
|
guard let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as? AddAttachmentTableViewCell else { return }
|
||||||
|
cell.setEnabled(isAddAttachmentsButtonEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
override func canPaste(_ itemProviders: [NSItemProvider]) -> Bool {
|
||||||
|
guard itemProviders.allSatisfy({ $0.canLoadObject(ofClass: CompositionAttachment.self) }) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mastodonController.instance.instanceType {
|
||||||
|
case .pleroma:
|
||||||
|
return true
|
||||||
|
case .mastodon:
|
||||||
|
return itemProviders.count + attachments.count <= 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func paste(itemProviders: [NSItemProvider]) {
|
||||||
|
for provider in itemProviders {
|
||||||
|
provider.loadObject(ofClass: CompositionAttachment.self) { (object, error) in
|
||||||
|
if let error = error {
|
||||||
|
fatalError("Couldn't load image from NSItemProvider: \(error)")
|
||||||
|
}
|
||||||
|
guard let attachment = object as? CompositionAttachment else {
|
||||||
|
fatalError("Couldn't convert object from NSItemProvider to CompositionAttachment")
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.attachments.append(attachment)
|
||||||
|
self.tableView.insertRows(at: [IndexPath(row: self.attachments.count - 1, section: 0)], with: .automatic)
|
||||||
|
self.updateHeightConstraint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadAll(stepProgress: @escaping () -> Void, completion: @escaping (_ success: Bool, _ uploadedAttachments: [Attachment]) -> Void) {
|
||||||
|
let group = DispatchGroup()
|
||||||
|
|
||||||
|
var anyFailed = false
|
||||||
|
var uploadedAttachments: [Result<Attachment, Error>?] = []
|
||||||
|
|
||||||
|
for (index, compAttachment) in attachments.enumerated() {
|
||||||
|
group.enter()
|
||||||
|
|
||||||
|
uploadedAttachments.append(nil)
|
||||||
|
|
||||||
|
compAttachment.data.getData { (data, mimeType) in
|
||||||
|
stepProgress()
|
||||||
|
|
||||||
|
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
||||||
|
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
||||||
|
self.mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
uploadedAttachments[index] = .failure(error)
|
||||||
|
anyFailed = true
|
||||||
|
case let .success(attachment, _):
|
||||||
|
uploadedAttachments[index] = .success(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
stepProgress()
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
if anyFailed {
|
||||||
|
let errors: [(Int, Error)] = uploadedAttachments.enumerated().compactMap { (index, result) in
|
||||||
|
switch result {
|
||||||
|
case let .failure(error):
|
||||||
|
return (index, error)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
let title: String
|
||||||
|
var message: String
|
||||||
|
if errors.count == 1 {
|
||||||
|
title = NSLocalizedString("Could not upload attachment", comment: "single attachment upload failed alert title")
|
||||||
|
message = errors[0].1.localizedDescription
|
||||||
|
} else {
|
||||||
|
title = NSLocalizedString("Could not upload the following attachments", comment: "multiple attachment upload failures alert title")
|
||||||
|
message = ""
|
||||||
|
for (index, error) in errors {
|
||||||
|
message.append("Attachment \(index + 1): \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
|
||||||
|
completion(false, [])
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
let uploadedAttachments: [Attachment] = uploadedAttachments.compactMap {
|
||||||
|
switch $0 {
|
||||||
|
case let .success(attachment):
|
||||||
|
return attachment
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completion(true, uploadedAttachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Table view data source
|
||||||
|
|
||||||
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
switch section {
|
||||||
|
case 0:
|
||||||
|
return attachments.count
|
||||||
|
case 1:
|
||||||
|
return 1
|
||||||
|
default:
|
||||||
|
fatalError("invalid section \(section)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
switch indexPath.section {
|
||||||
|
case 0:
|
||||||
|
let attachment = attachments[indexPath.row]
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "composeAttachment", for: indexPath) as! ComposeAttachmentTableViewCell
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(for: attachment)
|
||||||
|
cell.setEnabled(true)
|
||||||
|
return cell
|
||||||
|
case 1:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "addAttachment", for: indexPath) as! AddAttachmentTableViewCell
|
||||||
|
cell.setEnabled(isAddAttachmentsButtonEnabled())
|
||||||
|
return cell
|
||||||
|
default:
|
||||||
|
fatalError("invalid section \(indexPath.section)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
||||||
|
guard sourceIndexPath != destinationIndexPath, sourceIndexPath.section == 0, destinationIndexPath.section == 0 else { return }
|
||||||
|
|
||||||
|
attachments.insert(attachments.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Table view delegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||||
|
if indexPath.section == 1, isAddAttachmentsButtonEnabled() {
|
||||||
|
return indexPath
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
if indexPath.section == 1 {
|
||||||
|
addAttachmentPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
guard indexPath.section == 0 else { return nil }
|
||||||
|
|
||||||
|
let attachment = attachments[indexPath.row]
|
||||||
|
// cast to NSIndexPath because identifier needs to conform to NSCopying
|
||||||
|
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
|
||||||
|
return AssetPreviewViewController(attachment: attachment.data)
|
||||||
|
}) { (_) -> UIMenu? in
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func targetedPreview(forConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||||
|
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
|
||||||
|
let cell = tableView.cellForRow(at: indexPath) as? ComposeAttachmentTableViewCell {
|
||||||
|
let parameters = UIPreviewParameters()
|
||||||
|
parameters.backgroundColor = .black
|
||||||
|
return UITargetedPreview(view: cell.assetImageView, parameters: parameters)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||||
|
return targetedPreview(forConfiguration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||||
|
return targetedPreview(forConfiguration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Interaction
|
||||||
|
|
||||||
|
func addAttachmentPressed() {
|
||||||
|
if traitCollection.horizontalSizeClass == .compact {
|
||||||
|
let sheetContainer = AssetPickerSheetContainerViewController()
|
||||||
|
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||||
|
present(sheetContainer, animated: true)
|
||||||
|
} else {
|
||||||
|
let picker = AssetPickerViewController()
|
||||||
|
picker.assetPickerDelegate = self
|
||||||
|
picker.overrideUserInterfaceStyle = .dark
|
||||||
|
picker.modalPresentationStyle = .popover
|
||||||
|
present(picker, animated: true)
|
||||||
|
if let presentationController = picker.presentationController as? UIPopoverPresentationController {
|
||||||
|
presentationController.sourceView = tableView.cellForRow(at: IndexPath(row: 0, section: 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeAttachmentsViewController: UITableViewDragDelegate {
|
||||||
|
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
|
guard indexPath.section == 0 else { return [] }
|
||||||
|
|
||||||
|
let attachment = attachments[indexPath.row]
|
||||||
|
let provider = NSItemProvider(object: attachment)
|
||||||
|
let dragItem = UIDragItem(itemProvider: provider)
|
||||||
|
dragItem.localObject = attachment
|
||||||
|
return [dragItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
|
||||||
|
guard indexPath.section == 0 else { return [] }
|
||||||
|
|
||||||
|
let attachment = attachments[indexPath.row]
|
||||||
|
let provider = NSItemProvider(object: attachment)
|
||||||
|
let dragItem = UIDragItem(itemProvider: provider)
|
||||||
|
dragItem.localObject = attachment
|
||||||
|
return [dragItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, dragPreviewParametersForRowAt indexPath: IndexPath) -> UIDragPreviewParameters? {
|
||||||
|
guard indexPath.section == 0 else { return nil }
|
||||||
|
|
||||||
|
let cell = tableView.cellForRow(at: indexPath) as! ComposeAttachmentTableViewCell
|
||||||
|
let rect = cell.convert(cell.assetImageView.bounds, from: cell.assetImageView)
|
||||||
|
let path = UIBezierPath(roundedRect: rect, cornerRadius: cell.assetImageView.layer.cornerRadius)
|
||||||
|
let params = UIDragPreviewParameters()
|
||||||
|
params.visiblePath = path
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeAttachmentsViewController: UITableViewDropDelegate {
|
||||||
|
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
|
||||||
|
return session.canLoadObjects(ofClass: CompositionAttachment.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
|
||||||
|
// if items were dragged out of ourself, then the items are only being moved
|
||||||
|
if tableView.hasActiveDrag {
|
||||||
|
// todo: should moving multiple items actually be prohibited?
|
||||||
|
if session.items.count > 1 {
|
||||||
|
return UITableViewDropProposal(operation: .cancel)
|
||||||
|
} else {
|
||||||
|
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
|
||||||
|
let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(row: attachments.count, section: 0)
|
||||||
|
|
||||||
|
// we don't need to handle local items here, when the .move operation is used returned from the tableView(_:dropSessionDidUpdate:withDestinationIndexPath:) method,
|
||||||
|
// the table view will handle animating and call the normal data source tableView(_:moveRowAt:to:)
|
||||||
|
|
||||||
|
for (index, item) in coordinator.items.enumerated() {
|
||||||
|
let provider = item.dragItem.itemProvider
|
||||||
|
|
||||||
|
if provider.canLoadObject(ofClass: CompositionAttachment.self) {
|
||||||
|
let indexPath = IndexPath(row: destinationIndexPath.row + index, section: 0)
|
||||||
|
let placeholder = UITableViewDropPlaceholder(insertionIndexPath: indexPath, reuseIdentifier: "composeAttachment", rowHeight: 96)
|
||||||
|
placeholder.cellUpdateHandler = { (cell) in
|
||||||
|
let cell = cell as! ComposeAttachmentTableViewCell
|
||||||
|
cell.setEnabled(false)
|
||||||
|
}
|
||||||
|
let placeholderContext = coordinator.drop(item.dragItem, to: placeholder)
|
||||||
|
|
||||||
|
provider.loadObject(ofClass: CompositionAttachment.self) { (object, error) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let attachment = object as? CompositionAttachment {
|
||||||
|
placeholderContext.commitInsertion { (insertionIndexPath) in
|
||||||
|
self.attachments.insert(attachment, at: insertionIndexPath.row)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
placeholderContext.deletePlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHeightConstraint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeAttachmentsViewController: AssetPickerViewControllerDelegate {
|
||||||
|
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool {
|
||||||
|
switch mastodonController.instance.instanceType {
|
||||||
|
case .pleroma:
|
||||||
|
return true
|
||||||
|
case .mastodon:
|
||||||
|
if (type == .video && attachments.count > 0) ||
|
||||||
|
attachments.contains(where: { $0.data.type == .video }) ||
|
||||||
|
assetPicker.currentCollectionSelectedAssets.contains(where: { $0.type == .video }) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return attachments.count + assetPicker.currentCollectionSelectedAssets.count < 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData]) {
|
||||||
|
let attachments = attachments.map {
|
||||||
|
CompositionAttachment(data: $0)
|
||||||
|
}
|
||||||
|
let indexPaths = attachments.indices.map { IndexPath(row: $0 + self.attachments.count, section: 0) }
|
||||||
|
self.attachments.append(contentsOf: attachments)
|
||||||
|
tableView.insertRows(at: indexPaths, with: .automatic)
|
||||||
|
updateHeightConstraint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeAttachmentsViewController: ComposeAttachmentTableViewCellDelegate {
|
||||||
|
func removeAttachment(_ cell: ComposeAttachmentTableViewCell) {
|
||||||
|
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||||
|
attachments.remove(at: indexPath.row)
|
||||||
|
tableView.performBatchUpdates({
|
||||||
|
tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||||
|
}, completion: { (_) in
|
||||||
|
// when removing cells, we don't trigger the container height update until after the animation has completed
|
||||||
|
// otherwise, during the animation, the height is too short and the last row briefly disappears
|
||||||
|
self.updateHeightConstraint()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func attachmentDescriptionChanged(_ cell: ComposeAttachmentTableViewCell) {
|
||||||
|
delegate?.composeRequiresAttachmentDescriptionsDidChange()
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,11 +27,6 @@ class ComposeViewController: UIViewController {
|
||||||
visibilityChanged()
|
visibilityChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var selectedAttachments: [CompositionAttachment] = [] {
|
|
||||||
didSet {
|
|
||||||
updateAttachmentViews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasChanges = false
|
var hasChanges = false
|
||||||
var currentDraft: DraftsManager.Draft?
|
var currentDraft: DraftsManager.Draft?
|
||||||
|
@ -67,11 +62,12 @@ class ComposeViewController: UIViewController {
|
||||||
@IBOutlet weak var contentWarningContainerView: UIView!
|
@IBOutlet weak var contentWarningContainerView: UIView!
|
||||||
@IBOutlet weak var contentWarningTextField: UITextField!
|
@IBOutlet weak var contentWarningTextField: UITextField!
|
||||||
|
|
||||||
@IBOutlet weak var attachmentsStackView: UIStackView!
|
@IBOutlet weak var composeAttachmentsContainerView: UIView!
|
||||||
@IBOutlet weak var addAttachmentButton: UIButton!
|
|
||||||
|
|
||||||
@IBOutlet weak var postProgressView: SteppedProgressView!
|
@IBOutlet weak var postProgressView: SteppedProgressView!
|
||||||
|
|
||||||
|
var composeAttachmentsViewController: ComposeAttachmentsViewController!
|
||||||
|
|
||||||
init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, mastodonController: MastodonController) {
|
init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -141,9 +137,23 @@ class ComposeViewController: UIViewController {
|
||||||
|
|
||||||
// we have to set the font here, because the monospaced digit font is not available in IB
|
// we have to set the font here, because the monospaced digit font is not available in IB
|
||||||
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
|
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
|
||||||
updateCharactersRemaining()
|
|
||||||
updateAttachmentDescriptionsRequired()
|
|
||||||
updatePlaceholder()
|
updatePlaceholder()
|
||||||
|
// if the compose screen is opened via the home screen shortcut and app isn't running,
|
||||||
|
// the msatodon instance may not have been loaded yet
|
||||||
|
mastodonController.getOwnInstance { (_) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.updateCharactersRemaining()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeAttachmentsViewController = ComposeAttachmentsViewController(attachments: currentDraft?.attachments ?? [], mastodonController: mastodonController)
|
||||||
|
composeRequiresAttachmentDescriptionsDidChange()
|
||||||
|
composeAttachmentsViewController.delegate = self
|
||||||
|
composeAttachmentsViewController.tableView.isScrollEnabled = false
|
||||||
|
composeAttachmentsViewController.tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
embedChild(composeAttachmentsViewController, in: composeAttachmentsContainerView)
|
||||||
|
|
||||||
|
pasteConfiguration = composeAttachmentsViewController.pasteConfiguration
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
|
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
|
||||||
}
|
}
|
||||||
|
@ -218,26 +228,6 @@ class ComposeViewController: UIViewController {
|
||||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
|
||||||
super.viewDidLayoutSubviews()
|
|
||||||
|
|
||||||
// if inReplyToID != nil {
|
|
||||||
// scrollView.contentOffset = CGPoint(x: 0, y: stackView.arrangedSubviews.first!.frame.height)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
|
||||||
|
|
||||||
let imageName: String
|
|
||||||
if traitCollection.userInterfaceStyle == .dark {
|
|
||||||
imageName = "photo.fill"
|
|
||||||
} else {
|
|
||||||
imageName = "photo"
|
|
||||||
}
|
|
||||||
addAttachmentButton.setImage(UIImage(systemName: imageName), for: .normal)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createFormattingButtons() -> [UIBarButtonItem] {
|
func createFormattingButtons() -> [UIBarButtonItem] {
|
||||||
guard Preferences.shared.statusContentType != .plain else {
|
guard Preferences.shared.statusContentType != .plain else {
|
||||||
return []
|
return []
|
||||||
|
@ -279,23 +269,10 @@ class ComposeViewController: UIViewController {
|
||||||
scrollView.scrollIndicatorInsets = scrollView.contentInset
|
scrollView.scrollIndicatorInsets = scrollView.contentInset
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAttachmentDescriptionsRequired() {
|
|
||||||
if Preferences.shared.requireAttachmentDescriptions {
|
|
||||||
for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews {
|
|
||||||
if mediaView.descriptionTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
||||||
compositionState.formUnion(.requiresAttachmentDescriptions)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
compositionState.subtract(.requiresAttachmentDescriptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateCharactersRemaining() {
|
func updateCharactersRemaining() {
|
||||||
let count = CharacterCounter.count(text: statusTextView.text)
|
let count = CharacterCounter.count(text: statusTextView.text)
|
||||||
let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
|
let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
|
||||||
let remaining = (mastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
|
let remaining = (mastodonController.instance?.maxStatusCharacters ?? 500) - count - cwCount
|
||||||
if remaining < 0 {
|
if remaining < 0 {
|
||||||
charactersRemainingLabel.textColor = .red
|
charactersRemainingLabel.textColor = .red
|
||||||
compositionState.formUnion(.tooManyCharacters)
|
compositionState.formUnion(.tooManyCharacters)
|
||||||
|
@ -320,31 +297,6 @@ class ComposeViewController: UIViewController {
|
||||||
placeholderLabel.isHidden = !statusTextView.text.isEmpty
|
placeholderLabel.isHidden = !statusTextView.text.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAddAttachmentButton() {
|
|
||||||
switch mastodonController.instance.instanceType {
|
|
||||||
case .pleroma:
|
|
||||||
addAttachmentButton.isEnabled = true
|
|
||||||
case .mastodon:
|
|
||||||
addAttachmentButton.isEnabled = selectedAttachments.count <= 4 && !selectedAttachments.contains(where: { $0.type == .video })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateAttachmentViews() {
|
|
||||||
for view in attachmentsStackView.arrangedSubviews {
|
|
||||||
if view is ComposeMediaView {
|
|
||||||
view.removeFromSuperview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for attachment in selectedAttachments {
|
|
||||||
let mediaView = ComposeMediaView.create()
|
|
||||||
mediaView.delegate = self
|
|
||||||
mediaView.update(attachment: attachment)
|
|
||||||
attachmentsStackView.insertArrangedSubview(mediaView, at: attachmentsStackView.arrangedSubviews.count - 1)
|
|
||||||
updateAddAttachmentButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func contentWarningStateChanged() {
|
func contentWarningStateChanged() {
|
||||||
contentWarningContainerView.isHidden = !contentWarningEnabled
|
contentWarningContainerView.isHidden = !contentWarningEnabled
|
||||||
if contentWarningEnabled {
|
if contentWarningEnabled {
|
||||||
|
@ -360,13 +312,7 @@ class ComposeViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveDraft() {
|
func saveDraft() {
|
||||||
var attachments = [DraftsManager.DraftAttachment]()
|
let attachments = composeAttachmentsViewController.attachments
|
||||||
for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews
|
|
||||||
where mediaView.attachment.canSaveToDraft {
|
|
||||||
let attachment = mediaView.attachment!
|
|
||||||
let description = mediaView.descriptionTextView.text ?? ""
|
|
||||||
attachments.append(.init(attachment: attachment, description: description))
|
|
||||||
}
|
|
||||||
let statusText = statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
let statusText = statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let cw = contentWarningEnabled ? contentWarningTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) : nil
|
let cw = contentWarningEnabled ? contentWarningTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) : nil
|
||||||
let account = mastodonController.accountInfo!
|
let account = mastodonController.accountInfo!
|
||||||
|
@ -391,6 +337,14 @@ class ComposeViewController: UIViewController {
|
||||||
xcbSession?.complete(with: .cancel)
|
xcbSession?.complete(with: .cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func canPaste(_ itemProviders: [NSItemProvider]) -> Bool {
|
||||||
|
return composeAttachmentsViewController.canPaste(itemProviders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func paste(itemProviders: [NSItemProvider]) {
|
||||||
|
composeAttachmentsViewController.paste(itemProviders: itemProviders)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func showSaveAndClosePrompt() {
|
@objc func showSaveAndClosePrompt() {
|
||||||
|
@ -471,16 +425,6 @@ class ComposeViewController: UIViewController {
|
||||||
present(UINavigationController(rootViewController: draftsVC), animated: true)
|
present(UINavigationController(rootViewController: draftsVC), animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func addAttachmentPressed(_ sender: Any) {
|
|
||||||
// hide keyboard before showing asset picker, so it doesn't re-appear when asset picker is closed
|
|
||||||
contentWarningTextField.resignFirstResponder()
|
|
||||||
statusTextView.resignFirstResponder()
|
|
||||||
|
|
||||||
let sheetContainer = AssetPickerSheetContainerViewController()
|
|
||||||
sheetContainer.assetPicker.assetPickerDelegate = self
|
|
||||||
present(sheetContainer, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func postButtonPressed() {
|
@objc func postButtonPressed() {
|
||||||
guard let text = statusTextView.text,
|
guard let text = statusTextView.text,
|
||||||
!text.isEmpty else { return }
|
!text.isEmpty else { return }
|
||||||
|
@ -500,44 +444,16 @@ class ComposeViewController: UIViewController {
|
||||||
let sensitive = contentWarning != nil
|
let sensitive = contentWarning != nil
|
||||||
let visibility = self.visibility!
|
let visibility = self.visibility!
|
||||||
|
|
||||||
let group = DispatchGroup()
|
postProgressView.steps = 2 + (composeAttachmentsViewController.attachments.count * 2) // 2 steps (request data, then upload) for each attachment
|
||||||
|
|
||||||
var attachments: [Attachment?] = []
|
|
||||||
for compAttachment in selectedAttachments {
|
|
||||||
let index = attachments.count
|
|
||||||
attachments.append(nil)
|
|
||||||
|
|
||||||
let mediaView = attachmentsStackView.arrangedSubviews[index] as! ComposeMediaView
|
|
||||||
let description = mediaView.descriptionTextView.text
|
|
||||||
|
|
||||||
group.enter()
|
|
||||||
|
|
||||||
compAttachment.getData { (data, mimeType) in
|
|
||||||
self.postProgressView.step()
|
|
||||||
|
|
||||||
let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(attachment, _) = response else { fatalError() }
|
|
||||||
|
|
||||||
attachments[index] = attachment
|
|
||||||
|
|
||||||
self.postProgressView.step()
|
|
||||||
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
postProgressView.steps = 2 + (attachments.count * 2) // 2 steps (request data, then upload) for each attachment
|
|
||||||
postProgressView.currentStep = 1
|
postProgressView.currentStep = 1
|
||||||
|
|
||||||
group.notify(queue: .main) {
|
composeAttachmentsViewController.uploadAll(stepProgress: postProgressView.step) { (success, uploadedAttachments) in
|
||||||
let attachments = attachments.compactMap { $0 }
|
guard success else { return }
|
||||||
|
|
||||||
let request = Client.createStatus(text: text,
|
let request = Client.createStatus(text: text,
|
||||||
contentType: Preferences.shared.statusContentType,
|
contentType: Preferences.shared.statusContentType,
|
||||||
inReplyTo: self.inReplyToID,
|
inReplyTo: self.inReplyToID,
|
||||||
media: attachments,
|
media: uploadedAttachments,
|
||||||
sensitive: sensitive,
|
sensitive: sensitive,
|
||||||
spoilerText: contentWarning,
|
spoilerText: contentWarning,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
|
@ -555,6 +471,7 @@ class ComposeViewController: UIViewController {
|
||||||
self.postProgressView.step()
|
self.postProgressView.step()
|
||||||
self.dismiss(animated: true)
|
self.dismiss(animated: true)
|
||||||
|
|
||||||
|
// todo: this doesn't work
|
||||||
let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
|
let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
|
||||||
self.show(conversationVC, sender: self)
|
self.show(conversationVC, sender: self)
|
||||||
|
|
||||||
|
@ -594,36 +511,17 @@ extension ComposeViewController: UITextViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController: AssetPickerViewControllerDelegate {
|
extension ComposeViewController: ComposeAttachmentsViewControllerDelegate {
|
||||||
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool {
|
func composeSelectedAttachmentsDidChange() {
|
||||||
switch mastodonController.instance.instanceType {
|
currentDraft?.attachments = composeAttachmentsViewController.attachments
|
||||||
case .pleroma:
|
|
||||||
return true
|
|
||||||
case .mastodon:
|
|
||||||
if (type == .video && selectedAttachments.count > 0) ||
|
|
||||||
selectedAttachments.contains(where: { $0.type == .video }) ||
|
|
||||||
assetPicker.currentCollectionSelectedAssets.contains(where: { $0.type == .video }) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return selectedAttachments.count + assetPicker.currentCollectionSelectedAssets.count < 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) {
|
|
||||||
selectedAttachments.append(contentsOf: attachments)
|
|
||||||
updateAttachmentDescriptionsRequired()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController: ComposeMediaViewDelegate {
|
func composeRequiresAttachmentDescriptionsDidChange() {
|
||||||
func didRemoveMedia(_ mediaView: ComposeMediaView) {
|
if composeAttachmentsViewController.requiresAttachmentDescriptions {
|
||||||
let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)!
|
compositionState.formUnion(.requiresAttachmentDescriptions)
|
||||||
selectedAttachments.remove(at: index)
|
} else {
|
||||||
updateAddAttachmentButton()
|
compositionState.subtract(.requiresAttachmentDescriptions)
|
||||||
updateAttachmentDescriptionsRequired()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) {
|
|
||||||
updateAttachmentDescriptionsRequired()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,26 +563,16 @@ extension ComposeViewController: DraftsTableViewControllerDelegate {
|
||||||
updatePlaceholder()
|
updatePlaceholder()
|
||||||
updateCharactersRemaining()
|
updateCharactersRemaining()
|
||||||
|
|
||||||
selectedAttachments = draft.attachments.map { $0.attachment }
|
composeAttachmentsViewController.setAttachments(draft.attachments)
|
||||||
updateAttachmentViews()
|
|
||||||
|
|
||||||
for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews {
|
|
||||||
let attachment = draft.attachments.first(where: { $0.attachment == mediaView.attachment })!
|
|
||||||
mediaView.descriptionTextView.text = attachment.description
|
|
||||||
|
|
||||||
// call the delegate method manually, since setting the text property doesn't call it
|
|
||||||
mediaView.textViewDidChange(mediaView.descriptionTextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAttachmentDescriptionsRequired()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func draftSelectionCompleted() {
|
func draftSelectionCompleted() {
|
||||||
|
// todo: I don't think this can actually happen any more?
|
||||||
// check that all the assets from the draft have been added
|
// check that all the assets from the draft have been added
|
||||||
if let currentDraft = currentDraft, selectedAttachments.count < currentDraft.attachments.count {
|
if let currentDraft = currentDraft, composeAttachmentsViewController.attachments.count < currentDraft.attachments.count {
|
||||||
// some of the assets in the draft weren't loaded, so notify the user
|
// some of the assets in the draft weren't loaded, so notify the user
|
||||||
|
|
||||||
let difference = currentDraft.attachments.count - selectedAttachments.count
|
let difference = currentDraft.attachments.count - composeAttachmentsViewController.attachments.count
|
||||||
// todo: localize me
|
// todo: localize me
|
||||||
let suffix = difference == 1 ? "" : "s"
|
let suffix = difference == 1 ? "" : "s"
|
||||||
let verb = difference == 1 ? "was" : "were"
|
let verb = difference == 1 ? "was" : "were"
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<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"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ComposeViewController" customModule="Tusker" customModuleProvider="target">
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ComposeViewController" customModule="Tusker" customModuleProvider="target">
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="addAttachmentButton" destination="eEV-Yt-Njk" id="o9g-pP-dtd"/>
|
|
||||||
<outlet property="attachmentsStackView" destination="P0F-3w-gI1" id="Bi5-EK-N3a"/>
|
|
||||||
<outlet property="charactersRemainingLabel" destination="PMB-Wa-Ht0" id="PN9-wr-Pzu"/>
|
<outlet property="charactersRemainingLabel" destination="PMB-Wa-Ht0" id="PN9-wr-Pzu"/>
|
||||||
|
<outlet property="composeAttachmentsContainerView" destination="YFf-I2-7eX" id="u0n-Xe-v09"/>
|
||||||
<outlet property="contentView" destination="pcX-rB-RxJ" id="o95-Qa-6N7"/>
|
<outlet property="contentView" destination="pcX-rB-RxJ" id="o95-Qa-6N7"/>
|
||||||
<outlet property="contentWarningContainerView" destination="kU2-7l-MSy" id="Gnq-Jb-kCA"/>
|
<outlet property="contentWarningContainerView" destination="kU2-7l-MSy" id="Gnq-Jb-kCA"/>
|
||||||
<outlet property="contentWarningTextField" destination="T05-p6-vTz" id="Ivu-Ll-ByO"/>
|
<outlet property="contentWarningTextField" destination="T05-p6-vTz" id="Ivu-Ll-ByO"/>
|
||||||
|
@ -31,14 +30,14 @@
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" translatesAutoresizingMaskIntoConstraints="NO" id="6Z0-Vy-hMX">
|
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" alwaysBounceVertical="YES" keyboardDismissMode="interactive" translatesAutoresizingMaskIntoConstraints="NO" id="6Z0-Vy-hMX">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pcX-rB-RxJ">
|
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pcX-rB-RxJ">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="371.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="371.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="bOB-hF-O9w">
|
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="bOB-hF-O9w">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="371.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="419.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6V0-mH-Mhu">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6V0-mH-Mhu">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="66"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="66"/>
|
||||||
|
@ -134,56 +133,10 @@
|
||||||
<constraint firstAttribute="trailing" secondItem="9pn-0T-IHb" secondAttribute="trailing" constant="4" id="x7Z-8w-xgm"/>
|
<constraint firstAttribute="trailing" secondItem="9pn-0T-IHb" secondAttribute="trailing" constant="4" id="x7Z-8w-xgm"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="P0F-3w-gI1">
|
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YFf-I2-7eX">
|
||||||
<rect key="frame" x="0.0" y="291.5" width="375" height="80"/>
|
<rect key="frame" x="0.0" y="291.5" width="375" height="128"/>
|
||||||
<subviews>
|
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="752-dD-eAO">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
|
||||||
<subviews>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Aqk-LY-jEj">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="1"/>
|
|
||||||
<color key="backgroundColor" systemColor="separatorColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="1" id="0C7-KP-bIQ"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
</view>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cVn-xc-LH9">
|
|
||||||
<rect key="frame" x="0.0" y="79" width="375" height="1"/>
|
|
||||||
<color key="backgroundColor" systemColor="separatorColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="1" id="SZ4-5b-Hcf"/>
|
|
||||||
<constraint firstAttribute="height" constant="1" id="VIz-vl-Um4"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eEV-Yt-Njk">
|
|
||||||
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="80" id="sGZ-uD-CtS"/>
|
|
||||||
</constraints>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="18"/>
|
|
||||||
<state key="normal" title=" Add image or video" image="photo" catalog="system">
|
|
||||||
<color key="titleColor" systemColor="systemBlueColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
</state>
|
|
||||||
<connections>
|
|
||||||
<action selector="addAttachmentPressed:" destination="-1" eventType="touchUpInside" id="aUR-nx-O9u"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="Aqk-LY-jEj" firstAttribute="top" secondItem="752-dD-eAO" secondAttribute="top" id="6i1-Jt-AEM"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="eEV-Yt-Njk" secondAttribute="bottom" id="C6D-yq-PU3"/>
|
|
||||||
<constraint firstItem="Aqk-LY-jEj" firstAttribute="leading" secondItem="752-dD-eAO" secondAttribute="leading" id="Y5a-qr-Dby"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="cVn-xc-LH9" secondAttribute="bottom" id="dCy-ov-086"/>
|
|
||||||
<constraint firstItem="eEV-Yt-Njk" firstAttribute="leading" secondItem="752-dD-eAO" secondAttribute="leading" constant="8" id="enN-pq-hxK"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="Aqk-LY-jEj" secondAttribute="trailing" id="h1Q-QT-wB9"/>
|
|
||||||
<constraint firstItem="cVn-xc-LH9" firstAttribute="leading" secondItem="752-dD-eAO" secondAttribute="leading" id="oNI-gt-O9v"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="eEV-Yt-Njk" secondAttribute="trailing" constant="8" id="qe1-4r-oaa"/>
|
|
||||||
<constraint firstItem="eEV-Yt-Njk" firstAttribute="top" secondItem="752-dD-eAO" secondAttribute="top" id="rpc-rE-Q57"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="cVn-xc-LH9" secondAttribute="trailing" id="uSI-lv-mqY"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
</subviews>
|
|
||||||
</stackView>
|
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
|
@ -222,7 +175,4 @@
|
||||||
<point key="canvasLocation" x="140" y="154"/>
|
<point key="canvasLocation" x="140" y="154"/>
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
|
||||||
<image name="photo" catalog="system" width="64" height="46"/>
|
|
||||||
</resources>
|
|
||||||
</document>
|
</document>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
import Photos
|
import Photos
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
|
|
||||||
enum CompositionAttachment {
|
enum CompositionAttachmentData {
|
||||||
case asset(PHAsset)
|
case asset(PHAsset)
|
||||||
case image(UIImage)
|
case image(UIImage)
|
||||||
case video(URL)
|
case video(URL)
|
||||||
|
@ -79,7 +79,7 @@ enum CompositionAttachment {
|
||||||
options.version = .current
|
options.version = .current
|
||||||
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
||||||
guard let exportSession = exportSession else { fatalError("failed to create export session") }
|
guard let exportSession = exportSession else { fatalError("failed to create export session") }
|
||||||
CompositionAttachment.exportVideoData(session: exportSession, completion: completion)
|
CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fatalError("assetType must be either image or video")
|
fatalError("assetType must be either image or video")
|
||||||
|
@ -89,7 +89,7 @@ enum CompositionAttachment {
|
||||||
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
||||||
fatalError("failed to create export session")
|
fatalError("failed to create export session")
|
||||||
}
|
}
|
||||||
CompositionAttachment.exportVideoData(session: session, completion: completion)
|
CompositionAttachmentData.exportVideoData(session: session, completion: completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ enum CompositionAttachment {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PHAsset {
|
extension PHAsset {
|
||||||
var attachmentType: CompositionAttachment.AttachmentType? {
|
var attachmentType: CompositionAttachmentData.AttachmentType? {
|
||||||
switch self.mediaType {
|
switch self.mediaType {
|
||||||
case .image:
|
case .image:
|
||||||
return .image
|
return .image
|
||||||
|
@ -125,7 +125,7 @@ extension PHAsset {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CompositionAttachment: Codable {
|
extension CompositionAttachmentData: Codable {
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
@ -169,13 +169,15 @@ extension CompositionAttachment: Codable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CompositionAttachment: Equatable {
|
extension CompositionAttachmentData: Equatable {
|
||||||
static func ==(lhs: CompositionAttachment, rhs: CompositionAttachment) -> Bool {
|
static func ==(lhs: CompositionAttachmentData, rhs: CompositionAttachmentData) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.asset(a), .asset(b)):
|
case let (.asset(a), .asset(b)):
|
||||||
return a.localIdentifier == b.localIdentifier
|
return a.localIdentifier == b.localIdentifier
|
||||||
case let (.image(a), .image(b)):
|
case let (.image(a), .image(b)):
|
||||||
return a == b
|
return a == b
|
||||||
|
case let (.video(a), .video(b)):
|
||||||
|
return a == b
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,7 @@ class DraftsTableViewController: UITableViewController {
|
||||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||||
guard editingStyle == .delete else { return }
|
guard editingStyle == .delete else { return }
|
||||||
DraftsManager.shared.remove(draft(for: indexPath))
|
DraftsManager.shared.remove(draft(for: indexPath))
|
||||||
|
drafts.remove(at: indexPath.row)
|
||||||
tableView.deleteRows(at: [indexPath], with: .automatic)
|
tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,6 +238,33 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
return .delete
|
return .delete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
|
case .bookmarks:
|
||||||
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
||||||
|
return BookmarksTableViewController(mastodonController: self.mastodonController)
|
||||||
|
}, actionProvider: nil)
|
||||||
|
|
||||||
|
case let .list(list):
|
||||||
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
||||||
|
return ListTimelineViewController(for: list, mastodonController: self.mastodonController)
|
||||||
|
}, actionProvider: nil)
|
||||||
|
|
||||||
|
case let .savedHashtag(hashtag):
|
||||||
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
||||||
|
return HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
||||||
|
}, actionProvider: nil)
|
||||||
|
|
||||||
|
case let .savedInstance(url):
|
||||||
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
||||||
|
return InstanceTimelineViewController(for: url, parentMastodonController: self.mastodonController)
|
||||||
|
}, actionProvider: nil)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExploreViewController {
|
extension ExploreViewController {
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
// GalleryExpandAnimationController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/14/19.
|
|
||||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Gifu
|
|
||||||
|
|
||||||
class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
|
||||||
|
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
||||||
return 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from),
|
|
||||||
let toVC = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let containerView = transitionContext.containerView
|
|
||||||
containerView.addSubview(toVC.view)
|
|
||||||
|
|
||||||
let finalVCFrame = transitionContext.finalFrame(for: toVC)
|
|
||||||
guard let sourceInfo = toVC.sourcesInfo[toVC.startIndex],
|
|
||||||
let image = sourceInfo.image else {
|
|
||||||
toVC.view.frame = finalVCFrame
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let attachment = toVC.attachments[toVC.startIndex]
|
|
||||||
|
|
||||||
let ratio = image.size.width / image.size.height
|
|
||||||
var width = finalVCFrame.width
|
|
||||||
var height = width / ratio
|
|
||||||
let maxHeight = fromVC.view.bounds.height - fromVC.view.safeAreaInsets.top - fromVC.view.safeAreaInsets.bottom
|
|
||||||
if height > maxHeight {
|
|
||||||
let scaleFactor = maxHeight / height
|
|
||||||
width *= scaleFactor
|
|
||||||
height = maxHeight
|
|
||||||
}
|
|
||||||
let finalFrame = CGRect(x: finalVCFrame.midX - width / 2, y: finalVCFrame.midY - height / 2, width: width, height: height)
|
|
||||||
|
|
||||||
let imageView = GIFImageView(frame: sourceInfo.frame)
|
|
||||||
imageView.image = image
|
|
||||||
if attachment.url.pathExtension == "gif",
|
|
||||||
let data = ImageCache.attachments.get(attachment.url) {
|
|
||||||
imageView.animate(withGIFData: data)
|
|
||||||
}
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
imageView.layer.cornerRadius = sourceInfo.cornerRadius
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
|
|
||||||
let blackView = UIView(frame: finalVCFrame)
|
|
||||||
blackView.backgroundColor = .black
|
|
||||||
blackView.alpha = 0
|
|
||||||
|
|
||||||
containerView.addSubview(blackView)
|
|
||||||
containerView.addSubview(imageView)
|
|
||||||
|
|
||||||
toVC.view.isHidden = true
|
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
|
||||||
UIView.animate(withDuration: duration, animations: {
|
|
||||||
imageView.frame = finalFrame
|
|
||||||
imageView.layer.cornerRadius = 0
|
|
||||||
blackView.alpha = 1
|
|
||||||
}, completion: { _ in
|
|
||||||
toVC.view.frame = finalVCFrame
|
|
||||||
|
|
||||||
toVC.view.isHidden = false
|
|
||||||
fromVC.view.isHidden = false
|
|
||||||
blackView.removeFromSuperview()
|
|
||||||
imageView.removeFromSuperview()
|
|
||||||
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
// GalleryShrinkAnimationController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/14/19.
|
|
||||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Gifu
|
|
||||||
|
|
||||||
class GalleryShrinkAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
|
||||||
|
|
||||||
let interactionController: LargeImageInteractionController?
|
|
||||||
|
|
||||||
init(interactionController: LargeImageInteractionController?) {
|
|
||||||
self.interactionController = interactionController
|
|
||||||
}
|
|
||||||
|
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
||||||
return 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? GalleryViewController,
|
|
||||||
let toVC = transitionContext.viewController(forKey: .to) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let sourceInfo = fromVC.sourcesInfo[fromVC.currentIndex],
|
|
||||||
let image = fromVC.imageForDismissalAnimation() else {
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let originalVCFrame = fromVC.view.frame
|
|
||||||
|
|
||||||
let attachment = fromVC.attachments[fromVC.currentIndex]
|
|
||||||
|
|
||||||
let ratio = image.size.width / image.size.height
|
|
||||||
var width = originalVCFrame.width
|
|
||||||
var height = width / ratio
|
|
||||||
let maxHeight = fromVC.view.bounds.height - fromVC.view.safeAreaInsets.top - fromVC.view.safeAreaInsets.bottom
|
|
||||||
if height > maxHeight {
|
|
||||||
let scaleFactor = maxHeight / height
|
|
||||||
width *= scaleFactor
|
|
||||||
height = maxHeight
|
|
||||||
}
|
|
||||||
let originalFrame = CGRect(x: originalVCFrame.midX - width / 2, y: originalVCFrame.midY - height / 2, width: width, height: height)
|
|
||||||
|
|
||||||
let imageView = GIFImageView(frame: originalFrame)
|
|
||||||
imageView.image = image
|
|
||||||
if attachment.url.pathExtension == "gif",
|
|
||||||
let data = ImageCache.attachments.get(attachment.url) {
|
|
||||||
imageView.animate(withGIFData: data)
|
|
||||||
}
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
imageView.layer.cornerRadius = 0
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
|
|
||||||
let blackView = UIView(frame: originalVCFrame)
|
|
||||||
blackView.backgroundColor = .black
|
|
||||||
blackView.alpha = 1
|
|
||||||
|
|
||||||
let containerView = transitionContext.containerView
|
|
||||||
containerView.addSubview(toVC.view)
|
|
||||||
containerView.addSubview(blackView)
|
|
||||||
containerView.addSubview(imageView)
|
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
|
||||||
UIView.animate(withDuration: duration, animations: {
|
|
||||||
imageView.frame = sourceInfo.frame
|
|
||||||
imageView.layer.cornerRadius = sourceInfo.cornerRadius
|
|
||||||
blackView.alpha = 0
|
|
||||||
}, completion: { _ in
|
|
||||||
blackView.removeFromSuperview()
|
|
||||||
imageView.removeFromSuperview()
|
|
||||||
|
|
||||||
if transitionContext.transitionWasCancelled {
|
|
||||||
toVC.view.removeFromSuperview()
|
|
||||||
}
|
|
||||||
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,15 +7,15 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
|
||||||
import Photos
|
|
||||||
import Gifu
|
import Gifu
|
||||||
|
|
||||||
class LargeImageViewController: UIViewController, UIScrollViewDelegate {
|
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
|
||||||
|
|
||||||
typealias SourceInfo = (image: UIImage?, frame: CGRect, cornerRadius: CGFloat)
|
typealias SourceInfo = (image: UIImage?, frame: CGRect, cornerRadius: CGFloat)
|
||||||
|
|
||||||
var sourceInfo: SourceInfo?
|
var animationSourceInfo: SourceInfo?
|
||||||
|
var animationImage: UIImage? { animationSourceInfo?.image ?? image }
|
||||||
|
var animationGifData: Data? { gifData }
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
@IBOutlet weak var scrollView: UIScrollView!
|
@IBOutlet weak var scrollView: UIScrollView!
|
||||||
|
@ -62,7 +62,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate {
|
||||||
init(image: UIImage, description: String?, sourceInfo: SourceInfo?) {
|
init(image: UIImage, description: String?, sourceInfo: SourceInfo?) {
|
||||||
self.image = image
|
self.image = image
|
||||||
self.imageDescription = description
|
self.imageDescription = description
|
||||||
self.sourceInfo = sourceInfo
|
self.animationSourceInfo = sourceInfo
|
||||||
|
|
||||||
super.init(nibName: "LargeImageViewController", bundle: nil)
|
super.init(nibName: "LargeImageViewController", bundle: nil)
|
||||||
|
|
||||||
|
@ -128,10 +128,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageForDismissalAnimation() -> UIImage? {
|
|
||||||
return sourceInfo?.image ?? image
|
|
||||||
}
|
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
||||||
self.controlsVisible = controlsVisible
|
self.controlsVisible = controlsVisible
|
||||||
if animated {
|
if animated {
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
// LoadingLargeImageViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 6/14/19.
|
||||||
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController {
|
||||||
|
|
||||||
|
let url: URL
|
||||||
|
let cache: ImageCache
|
||||||
|
let imageDescription: String?
|
||||||
|
|
||||||
|
var largeImageVC: LargeImageViewController?
|
||||||
|
var loadingVC: LoadingViewController?
|
||||||
|
|
||||||
|
var imageRequest: ImageCache.Request?
|
||||||
|
|
||||||
|
private var initialControlsVisible: Bool = true
|
||||||
|
var controlsVisible: Bool {
|
||||||
|
get {
|
||||||
|
return largeImageVC?.controlsVisible ?? initialControlsVisible
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let largeImageVC = largeImageVC {
|
||||||
|
largeImageVC.setControlsVisible(newValue, animated: false)
|
||||||
|
} else {
|
||||||
|
initialControlsVisible = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shrinkGestureEnabled = true
|
||||||
|
|
||||||
|
var animationSourceInfo: LargeImageViewController.SourceInfo?
|
||||||
|
var animationImage: UIImage? { animationSourceInfo?.image ?? largeImageVC?.image }
|
||||||
|
var animationGifData: Data? { largeImageVC?.gifData }
|
||||||
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
|
override var prefersStatusBarHidden: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
override var childForHomeIndicatorAutoHidden: UIViewController? {
|
||||||
|
return largeImageVC
|
||||||
|
}
|
||||||
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
return .allButUpsideDown
|
||||||
|
} else {
|
||||||
|
return .all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(url: URL, cache: ImageCache, imageDescription: String?) {
|
||||||
|
self.url = url
|
||||||
|
self.cache = cache
|
||||||
|
self.imageDescription = imageDescription
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
modalPresentationStyle = .fullScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(attachment: Attachment) {
|
||||||
|
self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
overrideUserInterfaceStyle = .dark
|
||||||
|
view.backgroundColor = .black
|
||||||
|
|
||||||
|
if let data = cache.get(url) {
|
||||||
|
createLargeImage(data: data)
|
||||||
|
} else {
|
||||||
|
loadingVC = LoadingViewController()
|
||||||
|
embedChild(loadingVC!)
|
||||||
|
imageRequest = cache.get(url) { [weak self] (data) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.imageRequest = nil
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loadingVC?.removeViewAndController()
|
||||||
|
self.createLargeImage(data: data!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shrinkGestureEnabled {
|
||||||
|
dismissInteractionController = LargeImageInteractionController(viewController: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMove(toParent parent: UIViewController?) {
|
||||||
|
super.didMove(toParent: parent)
|
||||||
|
|
||||||
|
if parent == nil {
|
||||||
|
imageRequest?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLargeImage(data: Data) {
|
||||||
|
guard let image = UIImage(data: data) else { return }
|
||||||
|
largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceInfo: nil)
|
||||||
|
largeImageVC!.initialControlsVisible = initialControlsVisible
|
||||||
|
largeImageVC!.shrinkGestureEnabled = false
|
||||||
|
if url.pathExtension == "gif" {
|
||||||
|
largeImageVC!.gifData = data
|
||||||
|
}
|
||||||
|
embedChild(largeImageVC!)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,6 +9,13 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Gifu
|
import Gifu
|
||||||
|
|
||||||
|
protocol LargeImageAnimatableViewController: UIViewController {
|
||||||
|
var animationSourceInfo: LargeImageViewController.SourceInfo? { get }
|
||||||
|
var animationImage: UIImage? { get }
|
||||||
|
var animationGifData: Data? { get }
|
||||||
|
var dismissInteractionController: LargeImageInteractionController? { get }
|
||||||
|
}
|
||||||
|
|
||||||
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
|
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
|
@ -17,7 +24,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
|
|
||||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from),
|
guard let fromVC = transitionContext.viewController(forKey: .from),
|
||||||
let toVC = transitionContext.viewController(forKey: .to) as? LargeImageViewController else {
|
let toVC = transitionContext.viewController(forKey: .to) as? LargeImageAnimatableViewController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +32,8 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
containerView.addSubview(toVC.view)
|
containerView.addSubview(toVC.view)
|
||||||
|
|
||||||
let finalVCFrame = transitionContext.finalFrame(for: toVC)
|
let finalVCFrame = transitionContext.finalFrame(for: toVC)
|
||||||
guard let sourceInfo = toVC.sourceInfo,
|
guard let sourceInfo = toVC.animationSourceInfo,
|
||||||
let image = sourceInfo.image else {
|
let image = toVC.animationImage else {
|
||||||
toVC.view.frame = finalVCFrame
|
toVC.view.frame = finalVCFrame
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||||
return
|
return
|
||||||
|
@ -39,7 +46,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
|
|
||||||
let imageView = GIFImageView(frame: sourceInfo.frame)
|
let imageView = GIFImageView(frame: sourceInfo.frame)
|
||||||
imageView.image = image
|
imageView.image = image
|
||||||
if let gifData = toVC.gifData {
|
if let gifData = toVC.animationGifData {
|
||||||
imageView.animate(withGIFData: gifData)
|
imageView.animate(withGIFData: gifData)
|
||||||
}
|
}
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
|
@ -22,13 +22,13 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageViewController,
|
guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageAnimatableViewController,
|
||||||
let toVC = transitionContext.viewController(forKey: .to) else {
|
let toVC = transitionContext.viewController(forKey: .to) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let sourceInfo = fromVC.sourceInfo,
|
guard let sourceInfo = fromVC.animationSourceInfo,
|
||||||
let image = fromVC.imageForDismissalAnimation() else {
|
let image = fromVC.animationImage else {
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
|
|
||||||
let imageView = GIFImageView(frame: originalFrame)
|
let imageView = GIFImageView(frame: originalFrame)
|
||||||
imageView.image = image
|
imageView.image = image
|
||||||
if let gifData = fromVC.gifData {
|
if let gifData = fromVC.animationGifData {
|
||||||
imageView.animate(withGIFData: gifData)
|
imageView.animate(withGIFData: gifData)
|
||||||
}
|
}
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
|
@ -51,7 +51,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
|
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
|
||||||
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
|
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
|
||||||
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
|
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
|
||||||
tableView.register(UINib(nibName: "UnknownNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
||||||
|
|
||||||
tableView.prefetchDataSource = self
|
tableView.prefetchDataSource = self
|
||||||
|
|
||||||
|
@ -120,7 +120,9 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return tableView.dequeueReusableCell(withIdentifier: unknownCell, for: indexPath)
|
let cell = tableView.dequeueReusableCell(withIdentifier: unknownCell, for: indexPath)
|
||||||
|
cell.textLabel!.text = NSLocalizedString("Unknown Notification", comment: "unknown notification fallback cell text")
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,30 +7,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
//struct SilentActionPermission: Identifiable {
|
|
||||||
// let application: String
|
|
||||||
// let permission: Preferences.Permission
|
|
||||||
//
|
|
||||||
// var id: String {
|
|
||||||
// return application
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// init(_ application: String, _ permission: Preferences.Permission) {
|
|
||||||
// self.application = application
|
|
||||||
// self.permission = permission
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
struct SilentActionPrefs : View {
|
struct SilentActionPrefs : View {
|
||||||
// @MappedPreference(\.silentActions, fromPref: {
|
|
||||||
// var array = [SilentActionPermission]()
|
|
||||||
// for (application, permission) in $0 {
|
|
||||||
// array.append(SilentActionPermission(application, permission))
|
|
||||||
// }
|
|
||||||
// return array
|
|
||||||
// })
|
|
||||||
// var silentActionPermissions: [SilentActionPermission]
|
|
||||||
// @Preference(\.silentActions) var silentActions: [String: Preferences.Permission]
|
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -40,22 +17,12 @@ struct SilentActionPrefs : View {
|
||||||
.listStyle(GroupedListStyle())
|
.listStyle(GroupedListStyle())
|
||||||
// .navigationBarTitle("Silent Action Permissions")
|
// .navigationBarTitle("Silent Action Permissions")
|
||||||
// see FB6838291
|
// see FB6838291
|
||||||
// List(Array(silentActions.keys).identified(by: \.self)) { application in
|
|
||||||
// Text(application)
|
|
||||||
//// Toggle(isOn: Binding(getValue: { self.silentActions[application] == .accepted }, setValue: { self.silentActions[application] = $0 ? .accepted : .rejected }), label: Text(application))
|
|
||||||
// }.listStyle(.grouped)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SilentActionPermissionCell: View {
|
struct SilentActionPermissionCell: View {
|
||||||
@EnvironmentObject var preferences: Preferences
|
@ObservedObject var preferences = Preferences.shared
|
||||||
let source: String
|
let source: String
|
||||||
// var binding: Binding<Bool>
|
|
||||||
|
|
||||||
init(source: String) {
|
|
||||||
self.source = source
|
|
||||||
// self.binding = Binding(getValue: { self.preferences.silentActions[source] == .accepted }, setValue: { self.preferences.silentActions[source] = $0 ? .accepted : .rejected })
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Toggle(isOn: Binding(get: {
|
Toggle(isOn: Binding(get: {
|
||||||
|
|
|
@ -28,12 +28,12 @@ extension MenuPreviewProvider {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let account = mastodonController.cache.account(for: accountID) else { return [] }
|
let account = mastodonController.cache.account(for: accountID) else { return [] }
|
||||||
return [
|
return [
|
||||||
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
|
||||||
self.navigationDelegate?.selected(url: account.url)
|
|
||||||
}),
|
|
||||||
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { (_) in
|
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { (_) in
|
||||||
self.navigationDelegate?.compose(mentioning: account.acct)
|
self.navigationDelegate?.compose(mentioning: account.acct)
|
||||||
}),
|
}),
|
||||||
|
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
||||||
|
self.navigationDelegate?.selected(url: account.url)
|
||||||
|
}),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
||||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,7 +24,6 @@ class UserActivityManager {
|
||||||
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 }!
|
||||||
return window.rootViewController as! MainTabBarViewController
|
return window.rootViewController as! MainTabBarViewController
|
||||||
// return (UIApplication.shared.delegate! as! AppDelegate).window!.rootViewController as! MainTabBarViewController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func present(_ vc: UIViewController, animated: Bool = true) {
|
private static func present(_ vc: UIViewController, animated: Bool = true) {
|
||||||
|
|
|
@ -44,6 +44,10 @@ protocol TuskerNavigationDelegate: class {
|
||||||
|
|
||||||
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView)
|
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView)
|
||||||
|
|
||||||
|
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController
|
||||||
|
|
||||||
|
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView)
|
||||||
|
|
||||||
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController
|
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController
|
||||||
|
|
||||||
func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int)
|
func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int)
|
||||||
|
@ -183,6 +187,17 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
||||||
present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true)
|
present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
|
||||||
|
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
|
||||||
|
vc.animationSourceInfo = sourceViewInfo(sourceView)
|
||||||
|
vc.transitioningDelegate = self
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
|
||||||
|
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) {
|
||||||
|
present(loadingLargeImage(url: url, cache: cache, description: description, animatingFrom: sourceView), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
|
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
|
||||||
let sourcesInfo = sourceViews.map(sourceViewInfo)
|
let sourcesInfo = sourceViews.map(sourceViewInfo)
|
||||||
let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex)
|
let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex)
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftSoup
|
||||||
|
|
||||||
class AccountTableViewCell: UITableViewCell {
|
class AccountTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
|
@IBOutlet weak var noteLabel: EmojiLabel!
|
||||||
|
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
|
||||||
|
@ -54,6 +56,10 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
|
|
||||||
|
let doc = try! SwiftSoup.parse(account.note)
|
||||||
|
noteLabel.text = try! doc.text()
|
||||||
|
noteLabel.setEmojis(account.emojis, identifier: account.id)
|
||||||
|
|
||||||
updateUIForPrefrences()
|
updateUIForPrefrences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,40 +9,45 @@
|
||||||
<objects>
|
<objects>
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="AccountTableViewCell" customModule="Tusker" customModuleProvider="target">
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="100" id="KGk-i7-Jjw" customClass="AccountTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Rp2-O5-Vew">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Rp2-O5-Vew">
|
||||||
<rect key="frame" x="16" y="8" width="50" height="50"/>
|
<rect key="frame" x="16" y="8" width="50" height="50"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" secondItem="Rp2-O5-Vew" secondAttribute="height" multiplier="1:1" id="1AQ-lU-ptd"/>
|
<constraint firstAttribute="width" secondItem="Rp2-O5-Vew" secondAttribute="height" multiplier="1:1" id="1AQ-lU-ptd"/>
|
||||||
<constraint firstAttribute="height" priority="999" constant="50" id="NqI-m0-owe"/>
|
<constraint firstAttribute="height" constant="50" id="NqI-m0-owe"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</imageView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5">
|
||||||
<rect key="frame" x="74" y="11" width="230" height="44"/>
|
<rect key="frame" x="74" y="11" width="230" height="78"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fhc-bZ-lkB" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
<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="Fhc-bZ-lkB" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="230" height="26"/>
|
<rect key="frame" x="0.0" y="0.0" width="230" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JMo-QH-1is">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JMo-QH-1is">
|
||||||
<rect key="frame" x="0.0" y="26" width="230" height="18"/>
|
<rect key="frame" x="0.0" y="20.5" width="230" height="18"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
||||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Note" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bNO-qR-YEe" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="38.5" width="230" height="39.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="bottom" secondItem="Rp2-O5-Vew" secondAttribute="bottom" constant="8" id="Vw1-OF-tnw"/>
|
|
||||||
<constraint firstAttribute="bottomMargin" secondItem="Iif-9m-vM5" secondAttribute="bottom" id="dV0-Vm-DUb"/>
|
<constraint firstAttribute="bottomMargin" secondItem="Iif-9m-vM5" secondAttribute="bottom" id="dV0-Vm-DUb"/>
|
||||||
<constraint firstItem="Iif-9m-vM5" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="ihr-er-kLO"/>
|
<constraint firstItem="Iif-9m-vM5" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="ihr-er-kLO"/>
|
||||||
<constraint firstAttribute="trailingMargin" secondItem="Iif-9m-vM5" secondAttribute="trailing" id="q7a-DT-WPF"/>
|
<constraint firstAttribute="trailingMargin" secondItem="Iif-9m-vM5" secondAttribute="trailing" id="q7a-DT-WPF"/>
|
||||||
|
@ -55,9 +60,10 @@
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="avatarImageView" destination="Rp2-O5-Vew" id="3Gw-Xg-bd5"/>
|
<outlet property="avatarImageView" destination="Rp2-O5-Vew" id="3Gw-Xg-bd5"/>
|
||||||
<outlet property="displayNameLabel" destination="Fhc-bZ-lkB" id="1b0-3k-KR8"/>
|
<outlet property="displayNameLabel" destination="Fhc-bZ-lkB" id="1b0-3k-KR8"/>
|
||||||
|
<outlet property="noteLabel" destination="bNO-qR-YEe" id="4oO-c0-BOT"/>
|
||||||
<outlet property="usernameLabel" destination="JMo-QH-1is" id="ElX-ua-xcQ"/>
|
<outlet property="usernameLabel" destination="JMo-QH-1is" id="ElX-ua-xcQ"/>
|
||||||
</connections>
|
</connections>
|
||||||
<point key="canvasLocation" x="173.91304347826087" y="24.107142857142858"/>
|
<point key="canvasLocation" x="173.91304347826087" y="35.491071428571423"/>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
</objects>
|
</objects>
|
||||||
</document>
|
</document>
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
// AddAttachmentTableViewCell.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/13/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class AddAttachmentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
@IBOutlet weak var iconImageView: UIImageView!
|
||||||
|
@IBOutlet weak var label: UILabel!
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
let imageName: String
|
||||||
|
if traitCollection.userInterfaceStyle == .dark {
|
||||||
|
imageName = "photo.fill"
|
||||||
|
} else {
|
||||||
|
imageName = "photo"
|
||||||
|
}
|
||||||
|
iconImageView.image = UIImage(systemName: imageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEnabled(_ enabled: Bool) {
|
||||||
|
let color = enabled ? UIColor.systemBlue : .systemGray
|
||||||
|
iconImageView.tintColor = color
|
||||||
|
label.textColor = color
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?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="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="80" id="4Gv-Ok-KDT" customClass="AddAttachmentTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="80"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4Gv-Ok-KDT" id="wXX-bs-G7N">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="80"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="gMT-px-c1s">
|
||||||
|
<rect key="frame" x="8" y="0.0" width="398" height="80"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="photo" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="fgi-4Y-VXH">
|
||||||
|
<rect key="frame" x="0.0" y="31" width="24" height="17.5"/>
|
||||||
|
<color key="tintColor" systemColor="systemBlueColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Add image or video" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Du-B3-9rN">
|
||||||
|
<rect key="frame" x="40" y="30" width="358" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" systemColor="systemBlueColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="80" id="3h8-I7-wtl"/>
|
||||||
|
</constraints>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="gMT-px-c1s" firstAttribute="leading" secondItem="wXX-bs-G7N" secondAttribute="leading" constant="8" id="1Cz-v3-Rzq"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="gMT-px-c1s" secondAttribute="bottom" id="DFN-Nd-Baq"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="gMT-px-c1s" secondAttribute="trailing" constant="8" id="Omi-6C-4u6"/>
|
||||||
|
<constraint firstItem="gMT-px-c1s" firstAttribute="top" secondItem="wXX-bs-G7N" secondAttribute="top" id="TbI-3U-6aP"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<connections>
|
||||||
|
<outlet property="iconImageView" destination="fgi-4Y-VXH" id="hXw-M3-5B0"/>
|
||||||
|
<outlet property="label" destination="7Du-B3-9rN" id="yX4-nX-DnY"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="95.652173913043484" y="95.758928571428569"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="photo" catalog="system" width="128" height="93"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
|
@ -0,0 +1,89 @@
|
||||||
|
//
|
||||||
|
// ComposeAttachmentTableViewCell.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/13/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
//import Combine
|
||||||
|
import Photos
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
protocol ComposeAttachmentTableViewCellDelegate: class {
|
||||||
|
func removeAttachment(_ cell: ComposeAttachmentTableViewCell)
|
||||||
|
func attachmentDescriptionChanged(_ cell: ComposeAttachmentTableViewCell)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComposeAttachmentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
weak var delegate: ComposeAttachmentTableViewCellDelegate?
|
||||||
|
|
||||||
|
@IBOutlet weak var assetImageView: UIImageView!
|
||||||
|
@IBOutlet weak var descriptionTextView: UITextView!
|
||||||
|
@IBOutlet weak var descriptionPlaceholderLabel: UILabel!
|
||||||
|
@IBOutlet weak var removeButton: UIButton!
|
||||||
|
|
||||||
|
var attachment: CompositionAttachment!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
assetImageView.layer.masksToBounds = true
|
||||||
|
assetImageView.layer.cornerRadius = 8
|
||||||
|
|
||||||
|
descriptionTextView.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(for attachment: CompositionAttachment) {
|
||||||
|
self.attachment = attachment
|
||||||
|
|
||||||
|
descriptionTextView.text = attachment.attachmentDescription
|
||||||
|
updateDescriptionPlaceholderLabel()
|
||||||
|
|
||||||
|
switch attachment.data {
|
||||||
|
case let .image(image):
|
||||||
|
assetImageView.image = image
|
||||||
|
case let .asset(asset):
|
||||||
|
let size = CGSize(width: 80, height: 80)
|
||||||
|
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
|
||||||
|
guard self.attachment == attachment else { return }
|
||||||
|
self.assetImageView.image = image
|
||||||
|
}
|
||||||
|
case let .video(url):
|
||||||
|
let asset = AVURLAsset(url: url)
|
||||||
|
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
||||||
|
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
|
||||||
|
assetImageView.image = UIImage(cgImage: cgImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDescriptionPlaceholderLabel() {
|
||||||
|
descriptionPlaceholderLabel.isHidden = !descriptionTextView.text.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEnabled(_ enabled: Bool) {
|
||||||
|
descriptionTextView.isEditable = enabled
|
||||||
|
removeButton.isEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
assetImageView.image = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func removeButtonPressed(_ sender: Any) {
|
||||||
|
delegate?.removeAttachment(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeAttachmentTableViewCell: UITextViewDelegate {
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
attachment.attachmentDescription = textView.text
|
||||||
|
updateDescriptionPlaceholderLabel()
|
||||||
|
delegate?.attachmentDescriptionChanged(self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?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="retina6_1" 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"/>
|
||||||
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="96" id="KGk-i7-Jjw" customClass="ComposeAttachmentTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="96"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="96"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Describe for the visually impared..." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h6T-x4-yzl">
|
||||||
|
<rect key="frame" x="96" y="16" width="194" height="41"/>
|
||||||
|
<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>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="xRe-ec-Coh">
|
||||||
|
<rect key="frame" x="8" y="8" width="304" height="80"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="GLY-o8-47z">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
|
||||||
|
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="80" id="X6q-g9-dPN"/>
|
||||||
|
<constraint firstAttribute="height" constant="80" id="xgQ-E3-0QI"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="cwP-Eh-5dJ">
|
||||||
|
<rect key="frame" x="84" y="0.0" width="194" height="80"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
|
</textView>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Lvf-I9-aV3">
|
||||||
|
<rect key="frame" x="282" y="29" width="22" height="22"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="22" id="aIh-Ym-ARv"/>
|
||||||
|
<constraint firstAttribute="width" constant="22" id="qG5-np-4Bs"/>
|
||||||
|
</constraints>
|
||||||
|
<state key="normal" image="xmark.circle.fill" catalog="system"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="removeButtonPressed:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="efv-Xx-t89"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="cwP-Eh-5dJ" firstAttribute="height" secondItem="xRe-ec-Coh" secondAttribute="height" id="JPp-3t-8ow"/>
|
||||||
|
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="80" id="jWo-An-3h6"/>
|
||||||
|
</constraints>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="xRe-ec-Coh" secondAttribute="bottom" constant="8" id="DOS-Wv-G3s"/>
|
||||||
|
<constraint firstItem="xRe-ec-Coh" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="E41-OU-J0c"/>
|
||||||
|
<constraint firstItem="h6T-x4-yzl" firstAttribute="trailing" secondItem="cwP-Eh-5dJ" secondAttribute="trailing" constant="4" id="KN2-Ve-3B2"/>
|
||||||
|
<constraint firstItem="h6T-x4-yzl" firstAttribute="top" secondItem="cwP-Eh-5dJ" secondAttribute="top" constant="8" id="P3B-Jo-XMs"/>
|
||||||
|
<constraint firstItem="h6T-x4-yzl" firstAttribute="leading" secondItem="cwP-Eh-5dJ" secondAttribute="leading" constant="4" id="UjP-Gs-ZjO"/>
|
||||||
|
<constraint firstItem="xRe-ec-Coh" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="8" id="gRN-PV-gm6"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="xRe-ec-Coh" secondAttribute="trailing" constant="8" id="tyE-HK-4qb"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="assetImageView" destination="GLY-o8-47z" id="hZH-ur-m4z"/>
|
||||||
|
<outlet property="descriptionPlaceholderLabel" destination="h6T-x4-yzl" id="jBe-R0-Sfn"/>
|
||||||
|
<outlet property="descriptionTextView" destination="cwP-Eh-5dJ" id="pxJ-zF-GKC"/>
|
||||||
|
<outlet property="removeButton" destination="Lvf-I9-aV3" id="3qk-Zr-je1"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="107" y="181"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="xmark.circle.fill" catalog="system" width="128" height="121"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
|
@ -19,6 +19,7 @@ class AttachmentsContainerView: UIView {
|
||||||
var attachments: [Attachment]!
|
var attachments: [Attachment]!
|
||||||
|
|
||||||
let attachmentViews: NSHashTable<AttachmentView> = .weakObjects()
|
let attachmentViews: NSHashTable<AttachmentView> = .weakObjects()
|
||||||
|
var moreView: UIView?
|
||||||
|
|
||||||
var blurView: UIVisualEffectView?
|
var blurView: UIVisualEffectView?
|
||||||
var hideButtonView: UIVisualEffectView?
|
var hideButtonView: UIVisualEffectView?
|
||||||
|
@ -53,6 +54,7 @@ class AttachmentsContainerView: UIView {
|
||||||
|
|
||||||
attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
|
attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
|
||||||
attachmentViews.removeAllObjects()
|
attachmentViews.removeAllObjects()
|
||||||
|
moreView?.removeFromSuperview()
|
||||||
|
|
||||||
if attachments.count > 0 {
|
if attachments.count > 0 {
|
||||||
self.isHidden = false
|
self.isHidden = false
|
||||||
|
@ -128,6 +130,7 @@ class AttachmentsContainerView: UIView {
|
||||||
accessibilityElements.append(bottomRight)
|
accessibilityElements.append(bottomRight)
|
||||||
default: // more than 4
|
default: // more than 4
|
||||||
let moreView = UIView()
|
let moreView = UIView()
|
||||||
|
self.moreView = moreView
|
||||||
moreView.backgroundColor = .secondarySystemBackground
|
moreView.backgroundColor = .secondarySystemBackground
|
||||||
moreView.translatesAutoresizingMaskIntoConstraints = false
|
moreView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
moreView.isUserInteractionEnabled = true
|
moreView.isUserInteractionEnabled = true
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
//
|
|
||||||
// ComposeAttachmentView.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 1/10/19.
|
|
||||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Photos
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
protocol ComposeMediaViewDelegate: class {
|
|
||||||
func didRemoveMedia(_ mediaView: ComposeMediaView)
|
|
||||||
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComposeMediaView: UIView {
|
|
||||||
|
|
||||||
weak var delegate: ComposeMediaViewDelegate?
|
|
||||||
|
|
||||||
@IBOutlet weak var imageView: UIImageView!
|
|
||||||
@IBOutlet weak var descriptionTextView: UITextView!
|
|
||||||
@IBOutlet weak var placeholderLabel: UILabel!
|
|
||||||
|
|
||||||
var attachment: CompositionAttachment!
|
|
||||||
|
|
||||||
static func create() -> ComposeMediaView {
|
|
||||||
return UINib(nibName: "ComposeMediaView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeMediaView
|
|
||||||
}
|
|
||||||
|
|
||||||
override func awakeFromNib() {
|
|
||||||
super.awakeFromNib()
|
|
||||||
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
imageView.layer.cornerRadius = 10 // 0.1 * imageView.frame.width
|
|
||||||
|
|
||||||
descriptionTextView.delegate = self
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(attachment: CompositionAttachment) {
|
|
||||||
self.attachment = attachment
|
|
||||||
|
|
||||||
switch attachment {
|
|
||||||
case let .image(image):
|
|
||||||
imageView.image = image
|
|
||||||
case let .asset(asset):
|
|
||||||
let size = CGSize(width: 80, height: 80)
|
|
||||||
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
|
|
||||||
guard self.attachment == attachment else { return }
|
|
||||||
self.imageView.image = image
|
|
||||||
}
|
|
||||||
case let .video(url):
|
|
||||||
let asset = AVURLAsset(url: url)
|
|
||||||
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
|
||||||
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
|
|
||||||
imageView.image = UIImage(cgImage: cgImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interaction
|
|
||||||
|
|
||||||
@IBAction func removePressed(_ sender: Any) {
|
|
||||||
delegate?.didRemoveMedia(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ComposeMediaView: UITextViewDelegate {
|
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
|
||||||
placeholderLabel.isHidden = !descriptionTextView.text.isEmpty
|
|
||||||
delegate?.descriptionTextViewDidChange(self)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" 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="15704"/>
|
|
||||||
<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="ComposeMediaView" 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 userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="u7I-sx-kUe">
|
|
||||||
<rect key="frame" x="8" y="293.5" width="80" height="80"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="80" id="CgF-eC-We6"/>
|
|
||||||
<constraint firstAttribute="width" constant="80" id="S3g-yM-TRb"/>
|
|
||||||
</constraints>
|
|
||||||
</imageView>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G1g-Lw-Ren">
|
|
||||||
<rect key="frame" x="345" y="322.5" width="22" height="22"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="22" id="dyG-5Y-91s"/>
|
|
||||||
<constraint firstAttribute="width" constant="22" id="sWQ-3z-Z5r"/>
|
|
||||||
</constraints>
|
|
||||||
<state key="normal" image="xmark.circle.fill" catalog="system">
|
|
||||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
|
|
||||||
</state>
|
|
||||||
<connections>
|
|
||||||
<action selector="removePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="aor-Cq-YjJ"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="O6b-Zs-u8r">
|
|
||||||
<rect key="frame" x="96" y="0.0" width="241" height="667"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="80" id="GsE-uM-fhe"/>
|
|
||||||
</constraints>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
|
||||||
</textView>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Describe for the visually impaired..." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rkD-NP-09H">
|
|
||||||
<rect key="frame" x="100" y="8" width="233" height="41"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
|
||||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0h4-wv-2R8">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="1"/>
|
|
||||||
<color key="backgroundColor" systemColor="separatorColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="1" id="aQa-2T-uYY"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="O6b-Zs-u8r" secondAttribute="bottom" id="3sv-wo-gxe"/>
|
|
||||||
<constraint firstItem="u7I-sx-kUe" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="5Qs-i7-glv"/>
|
|
||||||
<constraint firstItem="u7I-sx-kUe" firstAttribute="top" relation="greaterThanOrEqual" secondItem="0h4-wv-2R8" secondAttribute="bottom" constant="8" id="86L-Cb-Lsk"/>
|
|
||||||
<constraint firstItem="O6b-Zs-u8r" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="9QY-MR-Yc2"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="0h4-wv-2R8" secondAttribute="trailing" id="FCS-un-JT6"/>
|
|
||||||
<constraint firstItem="u7I-sx-kUe" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="8" id="UWq-Lf-zGB"/>
|
|
||||||
<constraint firstItem="rkD-NP-09H" firstAttribute="leading" secondItem="O6b-Zs-u8r" secondAttribute="leading" constant="4" id="aQd-T9-n1I"/>
|
|
||||||
<constraint firstItem="0h4-wv-2R8" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="edp-4a-YGq"/>
|
|
||||||
<constraint firstItem="G1g-Lw-Ren" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="hJA-JF-MHp"/>
|
|
||||||
<constraint firstItem="O6b-Zs-u8r" firstAttribute="leading" secondItem="u7I-sx-kUe" secondAttribute="trailing" constant="8" id="hvZ-m4-TOV"/>
|
|
||||||
<constraint firstItem="G1g-Lw-Ren" firstAttribute="leading" secondItem="O6b-Zs-u8r" secondAttribute="trailing" constant="8" id="ith-0X-3Yz"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="u7I-sx-kUe" secondAttribute="bottom" constant="8" id="lGN-Qg-mBO"/>
|
|
||||||
<constraint firstItem="rkD-NP-09H" firstAttribute="top" secondItem="O6b-Zs-u8r" secondAttribute="top" constant="8" id="mRb-3M-7uz"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="G1g-Lw-Ren" secondAttribute="trailing" constant="8" id="o4F-MV-ahd"/>
|
|
||||||
<constraint firstItem="0h4-wv-2R8" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="sFo-wp-MYP"/>
|
|
||||||
<constraint firstItem="O6b-Zs-u8r" firstAttribute="trailing" secondItem="rkD-NP-09H" secondAttribute="trailing" constant="4" id="vwg-7l-8ca"/>
|
|
||||||
</constraints>
|
|
||||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
|
||||||
<connections>
|
|
||||||
<outlet property="descriptionTextView" destination="O6b-Zs-u8r" id="TNy-7h-0sY"/>
|
|
||||||
<outlet property="imageView" destination="u7I-sx-kUe" id="o3a-O0-m85"/>
|
|
||||||
<outlet property="placeholderLabel" destination="rkD-NP-09H" id="WtV-2h-L3n"/>
|
|
||||||
</connections>
|
|
||||||
<point key="canvasLocation" x="136.80000000000001" y="142.57871064467767"/>
|
|
||||||
</view>
|
|
||||||
</objects>
|
|
||||||
<resources>
|
|
||||||
<image name="xmark.circle.fill" catalog="system" width="64" height="60"/>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
|
@ -34,7 +34,7 @@ class DraftTableViewCell: UITableViewCell {
|
||||||
attachmentsStackView.addArrangedSubview(imageView)
|
attachmentsStackView.addArrangedSubview(imageView)
|
||||||
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
|
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
|
||||||
|
|
||||||
switch attachment.attachment {
|
switch attachment.data {
|
||||||
case let .asset(asset):
|
case let .asset(asset):
|
||||||
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
|
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
|
||||||
imageView.image = image
|
imageView.image = image
|
|
@ -149,36 +149,13 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func avatarPressed() {
|
@objc func avatarPressed() {
|
||||||
delegate?.showLargeImage(avatarImageView.image!, description: nil, animatingFrom: avatarImageView)
|
guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||||
|
delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func headerPressed() {
|
@objc func headerPressed() {
|
||||||
delegate?.showLargeImage(headerImageView.image!, description: nil, animatingFrom: headerImageView)
|
guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||||
|
delegate?.showLoadingLargeImage(url: account.header, cache: .headers, description: nil, animatingFrom: headerImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//extension ProfileHeaderTableViewCell: MenuPreviewProvider {
|
|
||||||
// var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
|
||||||
// func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
|
||||||
// let noteLabelPoint = noteLabel.convert(location, from: self)
|
|
||||||
// if noteLabel.bounds.contains(noteLabelPoint),
|
|
||||||
// let link = noteLabel.getLink(atPoint: noteLabelPoint) {
|
|
||||||
// return (
|
|
||||||
// content: { self.noteLabel.getViewController(forLink: link.url, inRange: link.range) },
|
|
||||||
// actions: {
|
|
||||||
// let text = (self.noteLabel.text! as NSString).substring(with: link.range)
|
|
||||||
// if let mention = self.noteLabel.getMention(for: link.url, text: text) {
|
|
||||||
// return self.actionsForProfile(accountID: mention.id, sourceView: self)
|
|
||||||
// } else if let hashtag = self.noteLabel.getHashtag(for: link.url, text: text) {
|
|
||||||
// return self.actionsForHashtag(hashtag, sourceView: self)
|
|
||||||
// } else {
|
|
||||||
// return self.actionsForURL(link.url, sourceView: self)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// } else {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var contentWarningLabel: UILabel!
|
@IBOutlet weak var contentWarningLabel: EmojiLabel!
|
||||||
@IBOutlet weak var collapseButton: UIButton!
|
@IBOutlet weak var collapseButton: UIButton!
|
||||||
@IBOutlet weak var contentTextView: StatusContentTextView!
|
@IBOutlet weak var contentTextView: StatusContentTextView!
|
||||||
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
||||||
|
@ -137,6 +137,9 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
contentWarningLabel.text = status.spoilerText
|
contentWarningLabel.text = status.spoilerText
|
||||||
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
||||||
|
if !contentWarningLabel.isHidden {
|
||||||
|
contentWarningLabel.setEmojis(status.emojis, identifier: statusID)
|
||||||
|
}
|
||||||
|
|
||||||
if state.unknown {
|
if state.unknown {
|
||||||
collapsible = !status.spoilerText.isEmpty
|
collapsible = !status.spoilerText.isEmpty
|
||||||
|
@ -188,7 +191,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func updateUIForPreferences() {
|
@objc func updateUIForPreferences() {
|
||||||
guard let account = mastodonController.cache.account(for: accountID) else { return }
|
guard let mastodonController = mastodonController, let account = mastodonController.cache.account(for: accountID) else { return }
|
||||||
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.cache.status(for: statusID)?.sensitive ?? false)
|
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.cache.status(for: statusID)?.sensitive ?? false)
|
||||||
|
@ -339,22 +342,7 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
|
||||||
actions: { [] }
|
actions: { [] }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}/* else if contentLabel.frame.contains(location),
|
|
||||||
let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) {
|
|
||||||
return (
|
|
||||||
content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) },
|
|
||||||
actions: {
|
|
||||||
let text = (self.contentLabel.text! as NSString).substring(with: link.range)
|
|
||||||
if let mention = self.contentLabel.getMention(for: link.url, text: text) {
|
|
||||||
return self.actionsForProfile(accountID: mention.id, sourceView: self)
|
|
||||||
} else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) {
|
|
||||||
return self.actionsForHashtag(hashtag, sourceView: self)
|
|
||||||
} else {
|
|
||||||
return self.actionsForURL(link.url, sourceView: self)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
}*/
|
|
||||||
return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController)
|
return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
<constraint firstItem="SWg-Ka-QyP" firstAttribute="top" secondItem="lZY-2e-17d" secondAttribute="bottom" id="lvX-1b-8cN"/>
|
<constraint firstItem="SWg-Ka-QyP" firstAttribute="top" secondItem="lZY-2e-17d" secondAttribute="bottom" id="lvX-1b-8cN"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cwQ-mR-L1b">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cwQ-mR-L1b" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="58" width="138" height="20.5"/>
|
<rect key="frame" x="0.0" y="58" width="138" height="20.5"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<accessibility key="accessibilityConfiguration">
|
||||||
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
||||||
|
|
|
@ -101,7 +101,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimestamp() {
|
func updateTimestamp() {
|
||||||
guard let mastodonController = mastodonController, let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
// 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
|
||||||
|
guard let mastodonController = mastodonController else { return }
|
||||||
|
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||||
|
|
||||||
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())
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
<constraint firstAttribute="height" secondItem="gll-xe-FSr" secondAttribute="height" id="B7p-Pc-fZD"/>
|
<constraint firstAttribute="height" secondItem="gll-xe-FSr" secondAttribute="height" id="B7p-Pc-fZD"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</stackView>
|
</stackView>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="755" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="inI-Og-YiU">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="755" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="inI-Og-YiU" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="24.5" width="277" height="20.5"/>
|
<rect key="frame" x="0.0" y="24.5" width="277" height="20.5"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<accessibility key="accessibilityConfiguration">
|
||||||
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
||||||
|
|
Loading…
Reference in New Issue