Compare commits

..

33 Commits

Author SHA1 Message Date
Shadowfacts 4ce8de280e
Bump build number 2020-03-17 21:58:14 -04:00
Shadowfacts 4018d39312
Fix double gestures in attachments gallery 2020-03-17 21:56:29 -04:00
Shadowfacts ae416bb604
Prevent crash if BaseStatusTableViewCell is leaked
If prefernces change and the the view controller the cell belongs to is dealloced, the
mastodonController will be nil, previously causing a crash.
2020-03-17 21:44:06 -04:00
Shadowfacts 5e9caf9179
Use LoadingLargeImageViewController for account avatar/header
Prevents crash when tapping unloaded avatar/header images
2020-03-17 21:42:09 -04:00
Shadowfacts 3bbbb05083
Rename AttachmentsViewController to LoadingLargeImageViewController and
make non-specific to attachments
2020-03-17 21:24:15 -04:00
Shadowfacts bd3e74c611
Remove unnecessary XIB 2020-03-17 21:07:44 -04:00
Shadowfacts 2e8c416e04
Merge gallery and large image animations 2020-03-17 21:05:45 -04:00
Shadowfacts 955f9e5916
Fix attachment descriptions not being set correctly 2020-03-17 21:03:29 -04:00
Shadowfacts 17f15db32d
Don't round bottom corners of asset picker
Corner radius doesn't match that used on 2019 iPad Pro, so rounding the
bottom corners results in the view controller beneath the asset picker
showing through in some split-screen configurations
2020-03-16 20:50:16 -04:00
Shadowfacts 1a11dd2a69
Present asset picker as popover in regular horizontal size class 2020-03-16 20:45:51 -04:00
Shadowfacts b5fa0bceab
Fix pasting using compose app shortcut while app isn't running 2020-03-16 19:09:25 -04:00
Shadowfacts c224d11417
Allow pasting and drag/dropping video attachments on compose screen 2020-03-16 19:05:58 -04:00
Shadowfacts bebf47f05c
Prevent incompatible items from being pasted on compose screen 2020-03-16 17:31:43 -04:00
Shadowfacts e76b719c6a
Add context menu previews to explore VC 2020-03-15 23:54:04 -04:00
Shadowfacts 478c7b7a23
Fix crash when long-presing add attachment button 2020-03-15 22:59:43 -04:00
Shadowfacts e3cc0df283
Remove unnecessary URL escaping 2020-03-15 21:09:11 -04:00
Shadowfacts 9ed05de3ee
Add compose attachments preview 2020-03-15 14:25:02 -04:00
Shadowfacts 64f41ea2b7
Fix crash when updating timeline status cell timestamp 2020-03-15 12:17:19 -04:00
Shadowfacts 9af4118dfc
Show truncated note in account cell 2020-03-15 11:56:41 -04:00
Shadowfacts 64a8f6d733
Reorganize code 2020-03-15 11:43:41 -04:00
Shadowfacts ca76568c79
Remove old code 2020-03-15 11:40:28 -04:00
Shadowfacts 18e91feb00
Fix requires attachment descriptions preference not working 2020-03-15 11:39:35 -04:00
Shadowfacts c5d2e9af68
Fix preferences/drafts not saving on iPad in some circumstances 2020-03-15 11:26:30 -04:00
Shadowfacts 0691c3b9d6
Fix asset preview size 2020-03-14 23:32:54 -04:00
Shadowfacts 1ccb450477
Support dragging and dropping attachments in the compose view controller
Allos dragging in attachments from other apps and drag/dropping with the
compose VC to reorder attachments
2020-03-14 20:08:36 -04:00
Shadowfacts 7117ce6320
Support pasting images to create attachments
Closes #91
2020-03-14 16:46:50 -04:00
Shadowfacts 34dccf1f37
Extract compose attachments into separate VC 2020-03-14 15:47:15 -04:00
Shadowfacts a3303dc8fb
Use same order for status and account preview actions 2020-03-11 22:54:38 -04:00
Shadowfacts d15fa2199e
Fix attachments container more view not beign removed on cell reuse
Closes #92
2020-03-11 22:49:53 -04:00
Shadowfacts fadddeda7f
Fix crash when deleting draft
Closes #94
2020-03-11 22:18:31 -04:00
Shadowfacts b232bec80f
Show custom emojis in content warnings
Closes #95
2020-03-11 21:56:35 -04:00
Shadowfacts 1b19a13b05
Decode status cards 2020-03-04 21:14:58 -05:00
Shadowfacts cd5b4c1145
Remove old code 2020-03-02 22:31:37 -05:00
56 changed files with 1552 additions and 879 deletions

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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;

View File

@ -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)
}
} }
} }

View File

@ -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
}
} }

View File

@ -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
} }

View File

@ -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>

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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() {

View File

@ -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()
} }

View File

@ -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 {

View File

@ -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)
} }
} }
} }

View File

@ -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!)

View File

@ -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
} }

View File

@ -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>

View File

@ -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()
}
}

View File

@ -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"

View File

@ -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>

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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 {

View File

@ -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)
})
}
}

View File

@ -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)
})
}
}

View File

@ -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 {

View File

@ -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!)
}
}

View File

@ -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

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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: {

View File

@ -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)
}) })

View File

@ -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) {

View File

@ -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)

View File

@ -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()
} }

View File

@ -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>

View File

@ -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
}
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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

View File

@ -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
// }
// }
//}

View File

@ -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)
} }
} }

View File

@ -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"/>

View File

@ -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())

View File

@ -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"/>