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)
self.id = try container.decode(String.self, forKey: .id)
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) {
self.remoteURL = URL(lenient: remote.replacingOccurrences(of: " ", with: "%20"))
self.remoteURL = URL(string: remote)!
} else {
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) {
self.textURL = URL(lenient: text.replacingOccurrences(of: " ", with: "%20"))
self.textURL = URL(string: text)!
} else {
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 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 {
case url
case title

View File

@ -36,6 +36,7 @@ public class Status: Decodable {
public let language: String?
public let pinned: Bool?
public let bookmarked: Bool?
public let card: Card?
public static func getContext(_ status: Status) -> Request<ConversationContext> {
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(status.id)/context")
@ -128,6 +129,7 @@ public class Status: Decodable {
case language
case pinned
case bookmarked
case card
}
}

View File

@ -7,14 +7,11 @@
objects = {
/* Begin PBXBuildFile section */
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041160FE22B442870030A9B7 /* AttachmentViewController.swift */; };
0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 041160FF22B442870030A9B7 /* AttachmentViewController.xib */; };
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */; };
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.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 */; };
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 */; };
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
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 */; };
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.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 */; };
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
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 */; };
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
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 */; };
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; };
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 */; };
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.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 */; };
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.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 */; };
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
@ -287,14 +288,11 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
041160FE22B442870030A9B7 /* AttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewController.swift; sourceTree = "<group>"; };
041160FF22B442870030A9B7 /* AttachmentViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AttachmentViewController.xib; sourceTree = "<group>"; };
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingLargeImageViewController.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>"; };
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>"; };
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>"; };
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; };
@ -303,8 +301,8 @@
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>"; };
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>"; };
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>"; };
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>"; };
@ -357,7 +355,7 @@
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>"; };
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>"; };
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>"; };
@ -386,9 +384,13 @@
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>"; };
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>"; };
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>"; };
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>"; };
@ -564,40 +566,12 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0411610422B4571E0030A9B7 /* Attachment */ = {
0411610522B457290030A9B7 /* Attachment Gallery */ = {
isa = PBXGroup;
children = (
041160FE22B442870030A9B7 /* AttachmentViewController.swift */,
041160FF22B442870030A9B7 /* AttachmentViewController.xib */,
);
path = Attachment;
sourceTree = "<group>";
};
0411610522B457290030A9B7 /* Gallery */ = {
isa = PBXGroup;
children = (
0411610622B457360030A9B7 /* Transitions */,
04D14BAE22B34A2800642648 /* GalleryViewController.swift */,
);
path = 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";
path = "Attachment Gallery";
sourceTree = "<group>";
};
D61099AC2144B0CC00432DC2 /* Pachyderm */ = {
@ -695,6 +669,37 @@
path = "Hashtag Cell";
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 */ = {
isa = PBXGroup;
children = (
@ -762,8 +767,6 @@
children = (
D627FF78217E950100CC0648 /* DraftsTableViewController.xib */,
D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */,
D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */,
D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */,
);
path = Drafts;
sourceTree = "<group>";
@ -790,12 +793,13 @@
D641C785213DD83B004B4513 /* Conversation */,
D641C786213DD852004B4513 /* Notifications */,
D641C787213DD862004B4513 /* Compose */,
D6B053A023BD2BED00A066FA /* Asset Picker */,
D627FF77217E94F200CC0648 /* Drafts */,
D627943C23A5635D00D38C68 /* Explore */,
D6BC9DD8232D8BCA002CA326 /* Search */,
D627944B23A9A02400D38C68 /* Lists */,
D641C788213DD86D004B4513 /* Large Image */,
0411610422B4571E0030A9B7 /* Attachment */,
0411610522B457290030A9B7 /* Gallery */,
0411610522B457290030A9B7 /* Attachment Gallery */,
D6A3BC822321F69400FD64D5 /* Account List */,
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
D627944823A6AD5100D38C68 /* Bookmarks */,
@ -861,13 +865,9 @@
D641C787213DD862004B4513 /* Compose */ = {
isa = PBXGroup;
children = (
D6B053A023BD2BED00A066FA /* Asset Picker */,
D627FF77217E94F200CC0648 /* Drafts */,
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */,
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
D626493423BD94CE00612E6E /* CompositionAttachment.swift */,
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
D620483123D2A6A3008A63EF /* CompositionState.swift */,
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */,
);
path = Compose;
sourceTree = "<group>";
@ -878,6 +878,7 @@
D646C954213B364600269FB5 /* Transitions */,
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */,
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */,
);
path = "Large Image";
sourceTree = "<group>";
@ -1129,8 +1130,9 @@
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
D67C57A721E2649B00C3118B /* Account Detail */,
D67C57B021E28F9400C3118B /* Compose Status Reply */,
D60C07E221E817560057FAA8 /* Compose Media */,
D626494023C122C800612E6E /* Asset Picker */,
D61959D1241E844900A37B8E /* Attachment Cells */,
D61959D0241E842400A37B8E /* Draft Cell */,
D641C78A213DD926004B4513 /* Status */,
D6C7D27B22B6EBE200071952 /* Attachments */,
D641C78B213DD92F004B4513 /* Profile Header */,
@ -1211,6 +1213,7 @@
D663626021360A9600C9CBA2 /* Preferences */,
D6AEBB3F2321640F00E5038B /* Activities */,
D667E5F62135C2ED0057A976 /* Extensions */,
D61959D2241E846D00A37B8E /* Models */,
D6F953F121251A2F00CF0F2B /* Controllers */,
D641C780213DD7C4004B4513 /* Screens */,
D6BED1722126661300F02DA0 /* Views */,
@ -1479,9 +1482,9 @@
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */,
0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */,
D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */,
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */,
D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */,
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
);
@ -1589,7 +1592,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */,
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */,
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
@ -1601,7 +1604,6 @@
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
@ -1612,7 +1614,7 @@
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
@ -1626,6 +1628,7 @@
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */,
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
@ -1634,7 +1637,6 @@
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */,
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
0454DDB122B467AA00B8BB8E /* GalleryShrinkAnimationController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
@ -1642,11 +1644,11 @@
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */,
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
@ -1656,6 +1658,7 @@
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */,
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
@ -1684,6 +1687,7 @@
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */,
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
@ -2014,7 +2018,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
@ -2039,7 +2043,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;

View File

@ -88,11 +88,16 @@ class MastodonController {
}
}
func getOwnInstance() {
let request = Client.getInstance()
run(request) { (response) in
guard case let .success(instance, _) = response else { fatalError() }
self.instance = instance
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
if let instance = self.instance {
completion?(instance)
} else {
let request = Client.getInstance()
run(request) { (response) in
guard case let .success(instance, _) = response else { fatalError() }
self.instance = instance
completion?(instance)
}
}
}

View File

@ -39,7 +39,7 @@ class DraftsManager: Codable {
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)
drafts.append(draft)
return draft
@ -58,11 +58,11 @@ extension DraftsManager {
private(set) var accountID: String
private(set) var text: String
private(set) var contentWarning: String?
private(set) var attachments: [DraftAttachment]
var attachments: [CompositionAttachment]
private(set) var inReplyToID: String?
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.accountID = accountID
self.text = text
@ -71,8 +71,8 @@ extension DraftsManager {
self.attachments = attachments
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.text = text
self.contentWarning = contentWarning
@ -84,9 +84,4 @@ extension DraftsManager {
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 {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let presented = presented as? LargeImageViewController,
presented.sourceInfo?.image != nil {
if let presented = presented as? LargeImageAnimatableViewController,
presented.animationImage != nil {
return LargeImageExpandAnimationController()
} else if let presented = presented as? GalleryViewController,
presented.sourcesInfo[presented.startIndex]?.image != nil {
return GalleryExpandAnimationController()
}
return nil
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let dismissed = dismissed as? LargeImageViewController,
dismissed.imageForDismissalAnimation() != nil {
if let dismissed = dismissed as? LargeImageAnimatableViewController,
dismissed.animationImage != nil {
return LargeImageShrinkAnimationController(interactionController: dismissed.dismissInteractionController)
} else if let dismissed = dismissed as? GalleryViewController,
dismissed.imageForDismissalAnimation() != nil {
return GalleryShrinkAnimationController(interactionController: dismissed.dismissInteractionController)
}
return nil
}
@ -36,10 +30,6 @@ extension UIViewController: UIViewControllerTransitioningDelegate {
let interactionController = animator.interactionController,
interactionController.inProgress {
return interactionController
} else if let animator = animator as? GalleryShrinkAnimationController,
let interactionController = animator.interactionController,
interactionController.inProgress {
return interactionController
}
return nil
}

View File

@ -98,5 +98,16 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</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>
</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.
// 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).
Preferences.save()
DraftsManager.save()
}
func sceneDidBecomeActive(_ scene: UIScene) {
@ -92,6 +95,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneWillResignActive(_ scene: UIScene) {
// 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).
Preferences.save()
DraftsManager.save()
}
func sceneWillEnterForeground(_ scene: UIScene) {
@ -103,9 +109,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// 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
// to restore the scene back to its current state.
Preferences.save()
DraftsManager.save()
}
func activateAccount(_ account: LocalData.UserAccountInfo) {

View File

@ -49,6 +49,21 @@ class AssetCollectionViewController: UICollectionViewController {
override func 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))
collectionView.alwaysBounceVertical = true
@ -170,12 +185,23 @@ class AssetCollectionViewController: UICollectionViewController {
return nil
} else {
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)
}, 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
@objc func donePressed() {

View File

@ -33,6 +33,8 @@ class AssetPickerSheetContainerViewController: SheetContainerViewController {
override func viewDidLoad() {
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()
}

View File

@ -10,15 +10,15 @@ import UIKit
import Photos
protocol AssetPickerViewControllerDelegate {
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment])
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData])
}
class AssetPickerViewController: UINavigationController {
var assetPickerDelegate: AssetPickerViewControllerDelegate?
var currentCollectionSelectedAssets: [CompositionAttachment] {
var currentCollectionSelectedAssets: [CompositionAttachmentData] {
if let vc = visibleViewController as? AssetCollectionViewController {
return vc.selectedAssets.map { .asset($0) }
} else {
@ -70,7 +70,7 @@ extension AssetPickerViewController: AssetCollectionViewControllerDelegate {
extension AssetPickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let attachment: CompositionAttachment
let attachment: CompositionAttachmentData
if let image = info[.originalImage] as? UIImage {
attachment = .image(image)
} else if let url = info[.mediaURL] as? URL {

View File

@ -13,14 +13,18 @@ import AVKit
class AssetPreviewViewController: UIViewController {
let asset: PHAsset
let attachment: CompositionAttachmentData
init(asset: PHAsset) {
self.asset = asset
init(attachment: CompositionAttachmentData) {
self.attachment = attachment
super.init(nibName: nil, bundle: nil)
}
convenience init(asset: PHAsset) {
self.init(attachment: .asset(asset))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@ -30,21 +34,29 @@ class AssetPreviewViewController: UIViewController {
view.backgroundColor = .black
if asset.mediaType == .image {
if asset.mediaSubtypes.contains(.photoLive) {
showLivePhoto()
} else {
showImage()
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) {
showLivePhoto(asset)
} else {
showAssetImage(asset)
}
case .video:
showAssetVideo(asset)
default:
fatalError("asset mediaType must be image or video")
}
} else if asset.mediaType == .video {
playVideo()
} else {
fatalError("asset mediaType must be image or video")
}
}
func showImage() {
let imageView = UIImageView()
func showImage(_ image: UIImage) {
let imageView = UIImageView(image: image)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
view.addSubview(imageView)
@ -54,7 +66,10 @@ class AssetPreviewViewController: UIViewController {
imageView.topAnchor.constraint(equalTo: view.topAnchor),
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
preferredContentSize = image.size
}
func showAssetImage(_ asset: PHAsset) {
let options = PHImageRequestOptions()
options.version = .current
options.deliveryMode = .opportunistic
@ -62,12 +77,12 @@ class AssetPreviewViewController: UIViewController {
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: asset, targetSize: view.bounds.size, contentMode: .aspectFit, options: options) { (image, _) in
DispatchQueue.main.async {
imageView.image = image
self.showImage(image!)
}
}
}
func showLivePhoto() {
func showLivePhoto(_ asset: PHAsset) {
let options = PHLivePhotoRequestOptions()
options.deliveryMode = .opportunistic
options.version = .current
@ -90,11 +105,23 @@ class AssetPreviewViewController: UIViewController {
livePhotoView.topAnchor.constraint(equalTo: self.view.topAnchor),
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()
options.deliveryMode = .automatic
options.isNetworkAccessAllowed = true
@ -104,13 +131,7 @@ class AssetPreviewViewController: UIViewController {
fatalError("failed to get AVAsset")
}
DispatchQueue.main.async {
let playerController = AVPlayerViewController()
let item = AVPlayerItem(asset: avAsset)
let player = AVPlayer(playerItem: item)
player.isMuted = true
player.play()
playerController.player = player
self.embedChild(playerController)
self.showVideo(asset: avAsset)
}
}
}

View File

@ -8,14 +8,16 @@
import UIKit
import Pachyderm
class AttachmentViewController: UIViewController {
let attachment: Attachment
class PendingLargeImageViewController: UIViewController {
let url: URL
let cache: ImageCache
let imageDescription: String?
var largeImageVC: LargeImageViewController?
var loadingVC: LoadingViewController?
var attachmentRequest: ImageCache.Request?
var imageRequest: ImageCache.Request?
private var initialControlsVisible: Bool = true
var controlsVisible: Bool {
@ -35,10 +37,16 @@ class AttachmentViewController: UIViewController {
return largeImageVC
}
init(attachment: Attachment) {
self.attachment = attachment
init(url: URL, cache: ImageCache, imageDescription: String?) {
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) {
@ -51,14 +59,14 @@ class AttachmentViewController: UIViewController {
overrideUserInterfaceStyle = .dark
view.backgroundColor = .black
if let data = ImageCache.attachments.get(attachment.url) {
if let data = cache.get(url) {
createLargeImage(data: data)
} else {
loadingVC = LoadingViewController()
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 }
self.attachmentRequest = nil
self.imageRequest = nil
DispatchQueue.main.async {
self.loadingVC?.removeViewAndController()
self.createLargeImage(data: data!)
@ -71,16 +79,16 @@ class AttachmentViewController: UIViewController {
super.didMove(toParent: parent)
if parent == nil {
attachmentRequest?.cancel()
imageRequest?.cancel()
}
}
func createLargeImage(data: Data) {
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!.shrinkGestureEnabled = false
if attachment.url.pathExtension == "gif" {
if url.pathExtension == "gif" {
largeImageVC!.gifData = data
}
embedChild(largeImageVC!)

View File

@ -10,10 +10,8 @@ import Pachyderm
import AVFoundation
import AVKit
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController {
var dismissInteractionController: LargeImageInteractionController?
let attachments: [Attachment]
let sourcesInfo: [LargeImageViewController.SourceInfo?]
let startIndex: Int
@ -27,6 +25,24 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
}
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 {
return true
@ -51,7 +67,9 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
self.pages = attachments.map {
switch $0.kind {
case .image:
return AttachmentViewController(attachment: $0)
let vc = LoadingLargeImageViewController(attachment: $0)
vc.shrinkGestureEnabled = false
return vc
case .video, .audio:
let vc = AVPlayerViewController()
vc.player = AVPlayer(url: $0.url)
@ -96,14 +114,6 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
vc.player?.play()
}
}
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
@ -125,8 +135,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
// MARK: - Page View Controller Delegate
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
if let pending = pendingViewControllers.first as? AttachmentViewController,
let current = viewControllers!.first as? AttachmentViewController {
if let pending = pendingViewControllers.first as? LoadingLargeImageViewController,
let current = viewControllers!.first as? LoadingLargeImageViewController {
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()
}
}
var selectedAttachments: [CompositionAttachment] = [] {
didSet {
updateAttachmentViews()
}
}
var hasChanges = false
var currentDraft: DraftsManager.Draft?
@ -67,11 +62,12 @@ class ComposeViewController: UIViewController {
@IBOutlet weak var contentWarningContainerView: UIView!
@IBOutlet weak var contentWarningTextField: UITextField!
@IBOutlet weak var attachmentsStackView: UIStackView!
@IBOutlet weak var addAttachmentButton: UIButton!
@IBOutlet weak var composeAttachmentsContainerView: UIView!
@IBOutlet weak var postProgressView: SteppedProgressView!
var composeAttachmentsViewController: ComposeAttachmentsViewController!
init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, mastodonController: MastodonController) {
self.mastodonController = mastodonController
@ -141,10 +137,24 @@ class ComposeViewController: UIViewController {
// we have to set the font here, because the monospaced digit font is not available in IB
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
updateCharactersRemaining()
updateAttachmentDescriptionsRequired()
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)
}
@ -217,27 +227,7 @@ class ComposeViewController: UIViewController {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, 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] {
guard Preferences.shared.statusContentType != .plain else {
return []
@ -279,23 +269,10 @@ class ComposeViewController: UIViewController {
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() {
let count = CharacterCounter.count(text: statusTextView.text)
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 {
charactersRemainingLabel.textColor = .red
compositionState.formUnion(.tooManyCharacters)
@ -320,31 +297,6 @@ class ComposeViewController: UIViewController {
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() {
contentWarningContainerView.isHidden = !contentWarningEnabled
if contentWarningEnabled {
@ -360,13 +312,7 @@ class ComposeViewController: UIViewController {
}
func saveDraft() {
var attachments = [DraftsManager.DraftAttachment]()
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 attachments = composeAttachmentsViewController.attachments
let statusText = statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let cw = contentWarningEnabled ? contentWarningTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) : nil
let account = mastodonController.accountInfo!
@ -391,6 +337,14 @@ class ComposeViewController: UIViewController {
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
@objc func showSaveAndClosePrompt() {
@ -470,17 +424,7 @@ class ComposeViewController: UIViewController {
draftsVC.delegate = self
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() {
guard let text = statusTextView.text,
!text.isEmpty else { return }
@ -500,48 +444,20 @@ class ComposeViewController: UIViewController {
let sensitive = contentWarning != nil
let visibility = self.visibility!
let group = DispatchGroup()
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.steps = 2 + (composeAttachmentsViewController.attachments.count * 2) // 2 steps (request data, then upload) for each attachment
postProgressView.currentStep = 1
group.notify(queue: .main) {
let attachments = attachments.compactMap { $0 }
composeAttachmentsViewController.uploadAll(stepProgress: postProgressView.step) { (success, uploadedAttachments) in
guard success else { return }
let request = Client.createStatus(text: text,
contentType: Preferences.shared.statusContentType,
inReplyTo: self.inReplyToID,
media: attachments,
sensitive: sensitive,
spoilerText: contentWarning,
visibility: visibility,
language: nil)
contentType: Preferences.shared.statusContentType,
inReplyTo: self.inReplyToID,
media: uploadedAttachments,
sensitive: sensitive,
spoilerText: contentWarning,
visibility: visibility,
language: nil)
self.mastodonController.run(request) { (response) in
guard case let .success(status, _) = response else { fatalError() }
self.postedStatus = status
@ -555,6 +471,7 @@ class ComposeViewController: UIViewController {
self.postProgressView.step()
self.dismiss(animated: true)
// todo: this doesn't work
let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
self.show(conversationVC, sender: self)
@ -594,36 +511,17 @@ extension ComposeViewController: UITextViewDelegate {
}
}
extension ComposeViewController: AssetPickerViewControllerDelegate {
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool {
switch mastodonController.instance.instanceType {
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 didRemoveMedia(_ mediaView: ComposeMediaView) {
let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)!
selectedAttachments.remove(at: index)
updateAddAttachmentButton()
updateAttachmentDescriptionsRequired()
extension ComposeViewController: ComposeAttachmentsViewControllerDelegate {
func composeSelectedAttachmentsDidChange() {
currentDraft?.attachments = composeAttachmentsViewController.attachments
}
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) {
updateAttachmentDescriptionsRequired()
func composeRequiresAttachmentDescriptionsDidChange() {
if composeAttachmentsViewController.requiresAttachmentDescriptions {
compositionState.formUnion(.requiresAttachmentDescriptions)
} else {
compositionState.subtract(.requiresAttachmentDescriptions)
}
}
}
@ -665,26 +563,16 @@ extension ComposeViewController: DraftsTableViewControllerDelegate {
updatePlaceholder()
updateCharactersRemaining()
selectedAttachments = draft.attachments.map { $0.attachment }
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()
composeAttachmentsViewController.setAttachments(draft.attachments)
}
func draftSelectionCompleted() {
// todo: I don't think this can actually happen any more?
// 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
let difference = currentDraft.attachments.count - selectedAttachments.count
let difference = currentDraft.attachments.count - composeAttachmentsViewController.attachments.count
// todo: localize me
let suffix = difference == 1 ? "" : "s"
let verb = difference == 1 ? "was" : "were"

View File

@ -1,17 +1,16 @@
<?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"/>
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ComposeViewController" customModule="Tusker" customModuleProvider="target">
<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="composeAttachmentsContainerView" destination="YFf-I2-7eX" id="u0n-Xe-v09"/>
<outlet property="contentView" destination="pcX-rB-RxJ" id="o95-Qa-6N7"/>
<outlet property="contentWarningContainerView" destination="kU2-7l-MSy" id="Gnq-Jb-kCA"/>
<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"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<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"/>
<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"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="bOB-hF-O9w">
<rect key="frame" x="0.0" y="0.0" width="375" height="371.5"/>
<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="419.5"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6V0-mH-Mhu">
<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"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="P0F-3w-gI1">
<rect key="frame" x="0.0" y="291.5" width="375" height="80"/>
<subviews>
<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 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>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YFf-I2-7eX">
<rect key="frame" x="0.0" y="291.5" width="375" height="128"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
</view>
</subviews>
</stackView>
</subviews>
@ -222,7 +175,4 @@
<point key="canvasLocation" x="140" y="154"/>
</view>
</objects>
<resources>
<image name="photo" catalog="system" width="64" height="46"/>
</resources>
</document>

View File

@ -10,7 +10,7 @@ import UIKit
import Photos
import MobileCoreServices
enum CompositionAttachment {
enum CompositionAttachmentData {
case asset(PHAsset)
case image(UIImage)
case video(URL)
@ -79,7 +79,7 @@ enum CompositionAttachment {
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") }
CompositionAttachment.exportVideoData(session: exportSession, completion: completion)
CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion)
}
} else {
fatalError("assetType must be either image or video")
@ -89,7 +89,7 @@ enum CompositionAttachment {
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
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 {
var attachmentType: CompositionAttachment.AttachmentType? {
var attachmentType: CompositionAttachmentData.AttachmentType? {
switch self.mediaType {
case .image:
return .image
@ -125,7 +125,7 @@ extension PHAsset {
}
}
extension CompositionAttachment: Codable {
extension CompositionAttachmentData: Codable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
@ -169,13 +169,15 @@ extension CompositionAttachment: Codable {
}
}
extension CompositionAttachment: Equatable {
static func ==(lhs: CompositionAttachment, rhs: CompositionAttachment) -> Bool {
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

@ -102,6 +102,7 @@ class DraftsTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
DraftsManager.shared.remove(draft(for: indexPath))
drafts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}

View File

@ -238,6 +238,33 @@ class ExploreViewController: EnhancedTableViewController {
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 {

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 Pachyderm
import Photos
import Gifu
class LargeImageViewController: UIViewController, UIScrollViewDelegate {
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
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?
@IBOutlet weak var scrollView: UIScrollView!
@ -62,7 +62,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate {
init(image: UIImage, description: String?, sourceInfo: SourceInfo?) {
self.image = image
self.imageDescription = description
self.sourceInfo = sourceInfo
self.animationSourceInfo = sourceInfo
super.init(nibName: "LargeImageViewController", bundle: nil)
@ -127,10 +127,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate {
closeButtonTrailingConstraint.constant = offset
}
}
func imageForDismissalAnimation() -> UIImage? {
return sourceInfo?.image ?? image
}
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
self.controlsVisible = controlsVisible

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 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 {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
@ -17,7 +24,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
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
}
@ -25,8 +32,8 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
containerView.addSubview(toVC.view)
let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let sourceInfo = toVC.sourceInfo,
let image = sourceInfo.image else {
guard let sourceInfo = toVC.animationSourceInfo,
let image = toVC.animationImage else {
toVC.view.frame = finalVCFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
@ -39,7 +46,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
let imageView = GIFImageView(frame: sourceInfo.frame)
imageView.image = image
if let gifData = toVC.gifData {
if let gifData = toVC.animationGifData {
imageView.animate(withGIFData: gifData)
}
imageView.contentMode = .scaleAspectFill

View File

@ -22,13 +22,13 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
}
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 {
return
}
guard let sourceInfo = fromVC.sourceInfo,
let image = fromVC.imageForDismissalAnimation() else {
guard let sourceInfo = fromVC.animationSourceInfo,
let image = fromVC.animationImage else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
}
@ -43,7 +43,7 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
let imageView = GIFImageView(frame: originalFrame)
imageView.image = image
if let gifData = fromVC.gifData {
if let gifData = fromVC.animationGifData {
imageView.animate(withGIFData: gifData)
}
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: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
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
@ -120,7 +120,9 @@ class NotificationsTableViewController: EnhancedTableViewController {
return cell
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
//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 {
// @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
var body: some View {
@ -40,23 +17,13 @@ struct SilentActionPrefs : View {
.listStyle(GroupedListStyle())
// .navigationBarTitle("Silent Action Permissions")
// 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 {
@EnvironmentObject var preferences: Preferences
@ObservedObject var preferences = Preferences.shared
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 {
Toggle(isOn: Binding(get: {
self.preferences.silentActions[self.source] == .accepted

View File

@ -28,12 +28,12 @@ extension MenuPreviewProvider {
guard let mastodonController = mastodonController,
let account = mastodonController.cache.account(for: accountID) else { 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
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
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 window = scene.windows.first { $0.isKeyWindow }!
return window.rootViewController as! MainTabBarViewController
// return (UIApplication.shared.delegate! as! AppDelegate).window!.rootViewController as! MainTabBarViewController
}
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 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 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)
}
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 {
let sourcesInfo = sourceViews.map(sourceViewInfo)
let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex)

View File

@ -7,6 +7,7 @@
//
import UIKit
import SwiftSoup
class AccountTableViewCell: UITableViewCell {
@ -16,6 +17,7 @@ class AccountTableViewCell: UITableViewCell {
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: EmojiLabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var noteLabel: EmojiLabel!
var accountID: String!
@ -25,7 +27,7 @@ class AccountTableViewCell: UITableViewCell {
super.awakeFromNib()
avatarImageView.layer.masksToBounds = true
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPrefrences), name: .preferencesChanged, object: nil)
}
@ -54,6 +56,10 @@ class AccountTableViewCell: UITableViewCell {
usernameLabel.text = "@\(account.acct)"
let doc = try! SwiftSoup.parse(account.note)
noteLabel.text = try! doc.text()
noteLabel.setEmojis(account.emojis, identifier: account.id)
updateUIForPrefrences()
}

View File

@ -9,40 +9,45 @@
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="AccountTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
<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="100"/>
<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="66"/>
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<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"/>
<constraints>
<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>
</imageView>
<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>
<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">
<rect key="frame" x="0.0" y="0.0" width="230" height="26"/>
<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="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</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">
<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"/>
<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>
<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>
</stackView>
</subviews>
<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 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"/>
@ -55,9 +60,10 @@
<connections>
<outlet property="avatarImageView" destination="Rp2-O5-Vew" id="3Gw-Xg-bd5"/>
<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"/>
</connections>
<point key="canvasLocation" x="173.91304347826087" y="24.107142857142858"/>
<point key="canvasLocation" x="173.91304347826087" y="35.491071428571423"/>
</tableViewCell>
</objects>
</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]!
let attachmentViews: NSHashTable<AttachmentView> = .weakObjects()
var moreView: UIView?
var blurView: UIVisualEffectView?
var hideButtonView: UIVisualEffectView?
@ -53,6 +54,7 @@ class AttachmentsContainerView: UIView {
attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
attachmentViews.removeAllObjects()
moreView?.removeFromSuperview()
if attachments.count > 0 {
self.isHidden = false
@ -128,6 +130,7 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(bottomRight)
default: // more than 4
let moreView = UIView()
self.moreView = moreView
moreView.backgroundColor = .secondarySystemBackground
moreView.translatesAutoresizingMaskIntoConstraints = false
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)
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
switch attachment.attachment {
switch attachment.data {
case let .asset(asset):
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
imageView.image = image

View File

@ -149,36 +149,13 @@ class ProfileHeaderTableViewCell: UITableViewCell {
}
@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() {
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 displayNameLabel: EmojiLabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var contentWarningLabel: UILabel!
@IBOutlet weak var contentWarningLabel: EmojiLabel!
@IBOutlet weak var collapseButton: UIButton!
@IBOutlet weak var contentTextView: StatusContentTextView!
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
@ -137,6 +137,9 @@ class BaseStatusTableViewCell: UITableViewCell {
contentWarningLabel.text = status.spoilerText
contentWarningLabel.isHidden = status.spoilerText.isEmpty
if !contentWarningLabel.isHidden {
contentWarningLabel.setEmojis(status.emojis, identifier: statusID)
}
if state.unknown {
collapsible = !status.spoilerText.isEmpty
@ -188,7 +191,7 @@ class BaseStatusTableViewCell: UITableViewCell {
}
@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)
displayNameLabel.updateForAccountDisplayName(account: account)
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.cache.status(for: statusID)?.sensitive ?? false)
@ -339,22 +342,7 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
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)
}
}

View File

@ -52,7 +52,7 @@
<constraint firstItem="SWg-Ka-QyP" firstAttribute="top" secondItem="lZY-2e-17d" secondAttribute="bottom" id="lvX-1b-8cN"/>
</constraints>
</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"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>

View File

@ -101,7 +101,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
}
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.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"/>
</constraints>
</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"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>