Rewrite Compose screen in SwiftUI
This commit is contained in:
parent
b55a96d649
commit
4c82b1a341
|
@ -314,12 +314,26 @@ public class Client {
|
|||
}
|
||||
|
||||
extension Client {
|
||||
public enum Error: Swift.Error {
|
||||
public enum Error: LocalizedError {
|
||||
case unknownError
|
||||
case invalidRequest
|
||||
case invalidResponse
|
||||
case invalidModel
|
||||
case mastodonError(String)
|
||||
|
||||
public var localizedDescription: String {
|
||||
switch self {
|
||||
case .unknownError:
|
||||
return "Unknown Error"
|
||||
case .invalidRequest:
|
||||
return "Invalid Request"
|
||||
case .invalidResponse:
|
||||
return "Invalid Response"
|
||||
case .invalidModel:
|
||||
return "Invalid Model"
|
||||
case .mastodonError(let error):
|
||||
return "Server Error: \(error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; };
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
|
||||
|
@ -73,10 +72,16 @@
|
|||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
||||
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483123D2A6A3008A63EF /* CompositionState.swift */; };
|
||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */; };
|
||||
D622757824EE133700B82A16 /* ComposeAssetPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622757724EE133700B82A16 /* ComposeAssetPicker.swift */; };
|
||||
D622757A24EE21D900B82A16 /* ComposeAttachmentRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622757924EE21D900B82A16 /* ComposeAttachmentRow.swift */; };
|
||||
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622759F24F1677200B82A16 /* ComposeHostingController.swift */; };
|
||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A524F1C81800B82A16 /* ComposeReplyView.swift */; };
|
||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
|
||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A924F1E01C00B82A16 /* ComposeTextView.swift */; };
|
||||
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
|
||||
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
|
||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; };
|
||||
|
@ -111,10 +116,6 @@
|
|||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
|
||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
|
||||
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 */; };
|
||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
||||
|
@ -150,7 +151,6 @@
|
|||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626121360B1900C9CBA2 /* Preferences.swift */; };
|
||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626321360D2300C9CBA2 /* AvatarStyle.swift */; };
|
||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
||||
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362702136338600C9CBA2 /* ComposeViewController.swift */; };
|
||||
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */; };
|
||||
D667383C23299340000A2373 /* InstanceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667383B23299340000A2373 /* InstanceType.swift */; };
|
||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
|
||||
|
@ -162,6 +162,10 @@
|
|||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; };
|
||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; };
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; };
|
||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284724ECBCB100C732D3 /* ComposeView.swift */; };
|
||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */; };
|
||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */; };
|
||||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284D24ECC01D00C732D3 /* Draft.swift */; };
|
||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
|
||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
|
||||
|
@ -177,6 +181,7 @@
|
|||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
|
||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; };
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||
|
@ -206,7 +211,6 @@
|
|||
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
||||
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
||||
D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; };
|
||||
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; };
|
||||
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* SceneDelegate.swift */; };
|
||||
D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; };
|
||||
D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.framework */; };
|
||||
|
@ -239,8 +243,13 @@
|
|||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
|
||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
|
||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
|
||||
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */; };
|
||||
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */; };
|
||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; };
|
||||
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */; };
|
||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */; };
|
||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */; };
|
||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; };
|
||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD6212518A200E1C4BB /* Assets.xcassets */; };
|
||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; };
|
||||
|
@ -331,7 +340,6 @@
|
|||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = "<group>"; };
|
||||
|
@ -385,10 +393,16 @@
|
|||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D620483123D2A6A3008A63EF /* CompositionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionState.swift; sourceTree = "<group>"; };
|
||||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentsList.swift; sourceTree = "<group>"; };
|
||||
D622757724EE133700B82A16 /* ComposeAssetPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAssetPicker.swift; sourceTree = "<group>"; };
|
||||
D622757924EE21D900B82A16 /* ComposeAttachmentRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentRow.swift; sourceTree = "<group>"; };
|
||||
D622759F24F1677200B82A16 /* ComposeHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeHostingController.swift; sourceTree = "<group>"; };
|
||||
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyView.swift; sourceTree = "<group>"; };
|
||||
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyContentView.swift; sourceTree = "<group>"; };
|
||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTextView.swift; sourceTree = "<group>"; };
|
||||
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; 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>"; };
|
||||
|
@ -422,10 +436,6 @@
|
|||
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>"; };
|
||||
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; 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>"; };
|
||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||
|
@ -465,7 +475,6 @@
|
|||
D663626121360B1900C9CBA2 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
D663626321360D2300C9CBA2 /* AvatarStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStyle.swift; sourceTree = "<group>"; };
|
||||
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
|
||||
D66362702136338600C9CBA2 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
||||
D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Visibility+Helpers.swift"; sourceTree = "<group>"; };
|
||||
D667383B23299340000A2373 /* InstanceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceType.swift; sourceTree = "<group>"; };
|
||||
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
|
||||
|
@ -477,6 +486,10 @@
|
|||
D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = "<group>"; };
|
||||
D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = "<group>"; };
|
||||
D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; };
|
||||
D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCurrentAccount.swift; sourceTree = "<group>"; };
|
||||
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAvatarImageView.swift; sourceTree = "<group>"; };
|
||||
D677284D24ECC01D00C732D3 /* Draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Draft.swift; sourceTree = "<group>"; };
|
||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
|
||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
||||
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
|
||||
|
@ -492,6 +505,7 @@
|
|||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
|
||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||
D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -520,7 +534,6 @@
|
|||
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
|
||||
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
|
||||
D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
|
||||
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = "<group>"; };
|
||||
D6AC956623C4347E008C9946 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
|
||||
D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
|
||||
|
@ -549,8 +562,13 @@
|
|||
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
|
||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
|
||||
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
||||
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = "<group>"; };
|
||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainComposeTextView.swift; sourceTree = "<group>"; };
|
||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
|
||||
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ConditionalModifier.swift"; sourceTree = "<group>"; };
|
||||
D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContainerView.swift; sourceTree = "<group>"; };
|
||||
D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeUIState.swift; sourceTree = "<group>"; };
|
||||
D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
@ -754,24 +772,14 @@
|
|||
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 */,
|
||||
D677284D24ECC01D00C732D3 /* Draft.swift */,
|
||||
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
@ -959,10 +967,20 @@
|
|||
D641C787213DD862004B4513 /* Compose */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */,
|
||||
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
|
||||
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */,
|
||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */,
|
||||
D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */,
|
||||
D622759F24F1677200B82A16 /* ComposeHostingController.swift */,
|
||||
D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */,
|
||||
D677284724ECBCB100C732D3 /* ComposeView.swift */,
|
||||
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */,
|
||||
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */,
|
||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
||||
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
||||
D622757924EE21D900B82A16 /* ComposeAttachmentRow.swift */,
|
||||
D622757724EE133700B82A16 /* ComposeAssetPicker.swift */,
|
||||
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */,
|
||||
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */,
|
||||
);
|
||||
path = Compose;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1081,6 +1099,7 @@
|
|||
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */,
|
||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */,
|
||||
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
|
||||
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1230,10 +1249,11 @@
|
|||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */,
|
||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
||||
D626494023C122C800612E6E /* Asset Picker */,
|
||||
D61959D1241E844900A37B8E /* Attachment Cells */,
|
||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||
D641C78A213DD926004B4513 /* Status */,
|
||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||
|
@ -1308,7 +1328,6 @@
|
|||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||
D6AC956623C4347E008C9946 /* SceneDelegate.swift */,
|
||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
||||
|
@ -1518,7 +1537,7 @@
|
|||
};
|
||||
D6D4DDCB212518A000E1C4BB = {
|
||||
CreatedOnToolsVersion = 10.0;
|
||||
LastSwiftMigration = 1020;
|
||||
LastSwiftMigration = 1200;
|
||||
};
|
||||
D6D4DDDF212518A200E1C4BB = {
|
||||
CreatedOnToolsVersion = 10.0;
|
||||
|
@ -1604,11 +1623,8 @@
|
|||
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
|
||||
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
|
||||
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */,
|
||||
D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */,
|
||||
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
||||
D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */,
|
||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
|
||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1723,6 +1739,7 @@
|
|||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||
|
@ -1738,10 +1755,13 @@
|
|||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
||||
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */,
|
||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||
D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */,
|
||||
|
@ -1750,7 +1770,6 @@
|
|||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
|
||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
|
||||
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */,
|
||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||
|
@ -1760,13 +1779,13 @@
|
|||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */,
|
||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,
|
||||
|
@ -1777,6 +1796,7 @@
|
|||
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
|
||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */,
|
||||
|
@ -1785,11 +1805,12 @@
|
|||
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
||||
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.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 */,
|
||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||
|
@ -1803,6 +1824,7 @@
|
|||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
|
||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
||||
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||
|
@ -1815,6 +1837,7 @@
|
|||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
|
||||
D627943E23A564D400D38C68 /* ExploreViewController.swift in Sources */,
|
||||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
|
||||
D622757A24EE21D900B82A16 /* ComposeAttachmentRow.swift in Sources */,
|
||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */,
|
||||
D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */,
|
||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
||||
|
@ -1823,6 +1846,7 @@
|
|||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||
|
@ -1830,10 +1854,11 @@
|
|||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||
D622757824EE133700B82A16 /* ComposeAssetPicker.swift in Sources */,
|
||||
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
|
||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
||||
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */,
|
||||
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */,
|
||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
|
||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
|
||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||
|
@ -1852,9 +1877,10 @@
|
|||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
||||
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
||||
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */,
|
||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||
|
@ -1872,7 +1898,9 @@
|
|||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
||||
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */,
|
||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
||||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
|
||||
|
|
|
@ -28,7 +28,9 @@ class SendMessageActivity: AccountActivity {
|
|||
override var activityViewController: UIViewController? {
|
||||
guard let account = account else { return nil }
|
||||
|
||||
return UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController))
|
||||
let draft = mastodonController.createDraft(mentioningAcct: account.acct)
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
||||
return UINavigationController(rootViewController: compose)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -116,3 +116,6 @@ class MastodonController {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// ObservableObject so that SwiftUI views can receive it through @EnvironmentObject
|
||||
extension MastodonController: ObservableObject {}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
//
|
||||
// DraftsManager.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 10/22/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class DraftsManager: Codable {
|
||||
|
||||
private(set) static var shared: DraftsManager = load()
|
||||
|
||||
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
private static var archiveURL = DraftsManager.documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
|
||||
|
||||
static func save() {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(shared)
|
||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||
}
|
||||
}
|
||||
|
||||
static func load() -> DraftsManager {
|
||||
let decoder = PropertyListDecoder()
|
||||
if let data = try? Data(contentsOf: archiveURL),
|
||||
let draftsManager = try? decoder.decode(DraftsManager.self, from: data) {
|
||||
return draftsManager
|
||||
}
|
||||
return DraftsManager()
|
||||
}
|
||||
|
||||
private init() {}
|
||||
|
||||
var drafts: [Draft] = []
|
||||
var sorted: [Draft] {
|
||||
return drafts.sorted(by: { $0.lastModified > $1.lastModified })
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func remove(_ draft: Draft) {
|
||||
let index = drafts.firstIndex(of: draft)!
|
||||
drafts.remove(at: index)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DraftsManager {
|
||||
class Draft: Codable, Equatable {
|
||||
let id: UUID
|
||||
private(set) var accountID: String
|
||||
private(set) var text: String
|
||||
private(set) var contentWarning: String?
|
||||
var attachments: [CompositionAttachment]
|
||||
private(set) var inReplyToID: String?
|
||||
private(set) var lastModified: Date
|
||||
|
||||
init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [CompositionAttachment], lastModified: Date = Date()) {
|
||||
self.id = UUID()
|
||||
self.accountID = accountID
|
||||
self.text = text
|
||||
self.contentWarning = contentWarning
|
||||
self.inReplyToID = inReplyToID
|
||||
self.attachments = attachments
|
||||
self.lastModified = lastModified
|
||||
}
|
||||
|
||||
func update(accountID: String, text: String, contentWarning: String?, attachments: [CompositionAttachment]) {
|
||||
self.accountID = accountID
|
||||
self.text = text
|
||||
self.contentWarning = contentWarning
|
||||
self.lastModified = Date()
|
||||
self.attachments = attachments
|
||||
}
|
||||
|
||||
static func ==(lhs: Draft, rhs: Draft) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// View+ConditionalModifier.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/31/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
|
||||
@ViewBuilder
|
||||
func conditionally<Modified: View>(_ condition: Bool, modifier: (Self) -> Modified) -> some View {
|
||||
if condition {
|
||||
modifier(self)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -10,22 +10,48 @@ import Foundation
|
|||
import UIKit
|
||||
import MobileCoreServices
|
||||
|
||||
final class CompositionAttachment: NSObject, Codable {
|
||||
final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
||||
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
||||
|
||||
let data: CompositionAttachmentData
|
||||
var attachmentDescription: String
|
||||
let id: UUID
|
||||
@Published var data: CompositionAttachmentData
|
||||
@Published var attachmentDescription: String
|
||||
|
||||
init(data: CompositionAttachmentData, description: String = "") {
|
||||
self.id = UUID()
|
||||
self.data = data
|
||||
self.attachmentDescription = description
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.id = try container.decode(UUID.self, forKey: .id)
|
||||
self.data = try container.decode(CompositionAttachmentData.self, forKey: .data)
|
||||
self.attachmentDescription = try container.decode(String.self, forKey: .attachmentDescription)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(attachmentDescription, forKey: .attachmentDescription)
|
||||
}
|
||||
|
||||
static func ==(lhs: CompositionAttachment, rhs: CompositionAttachment) -> Bool {
|
||||
return lhs.data == rhs.data
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case data
|
||||
case attachmentDescription
|
||||
}
|
||||
}
|
||||
|
||||
extension CompositionAttachment: Identifiable {}
|
||||
|
||||
private let imageType = kUTTypeImage as String
|
||||
private let mp4Type = kUTTypeMPEG4 as String
|
||||
private let quickTimeType = kUTTypeQuickTimeMovie as String
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// CompositionState.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/17/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CompositionState: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
static let currentlyPosting = CompositionState(rawValue: 1 << 0)
|
||||
static let tooManyCharacters = CompositionState(rawValue: 1 << 1)
|
||||
static let requiresAttachmentDescriptions = CompositionState(rawValue: 1 << 2)
|
||||
|
||||
static let valid: CompositionState = []
|
||||
|
||||
var isValid: Bool {
|
||||
isEmpty
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// Draft.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/18/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
class Draft: Codable, ObservableObject {
|
||||
let id: UUID
|
||||
var lastModified: Date
|
||||
|
||||
@Published var accountID: String
|
||||
@Published var text: String
|
||||
@Published var contentWarningEnabled: Bool
|
||||
@Published var contentWarning: String
|
||||
@Published var attachments: [CompositionAttachment]
|
||||
@Published var inReplyToID: String?
|
||||
@Published var visibility: Status.Visibility
|
||||
|
||||
var initialText: String
|
||||
|
||||
var hasContent: Bool {
|
||||
(!text.isEmpty && text != initialText) ||
|
||||
(contentWarningEnabled && !contentWarning.isEmpty) ||
|
||||
attachments.count > 0
|
||||
}
|
||||
|
||||
init(accountID: String) {
|
||||
self.id = UUID()
|
||||
self.lastModified = Date()
|
||||
|
||||
self.accountID = accountID
|
||||
self.text = ""
|
||||
self.contentWarningEnabled = false
|
||||
self.contentWarning = ""
|
||||
self.attachments = []
|
||||
self.inReplyToID = nil
|
||||
self.visibility = Preferences.shared.defaultPostVisibility
|
||||
|
||||
self.initialText = ""
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.id = try container.decode(UUID.self, forKey: .id)
|
||||
self.lastModified = try container.decode(Date.self, forKey: .lastModified)
|
||||
|
||||
self.accountID = try container.decode(String.self, forKey: .accountID)
|
||||
self.text = try container.decode(String.self, forKey: .text)
|
||||
if let enabled = try? container.decode(Bool.self, forKey: .contentWarningEnabled) {
|
||||
self.contentWarningEnabled = enabled
|
||||
self.contentWarning = try container.decode(String.self, forKey: .contentWarning)
|
||||
} else {
|
||||
// todo: temporary until migration away from old drafts manager is complete
|
||||
let cw = try container.decode(String?.self, forKey: .contentWarning)
|
||||
if let cw = cw {
|
||||
self.contentWarningEnabled = !cw.isEmpty
|
||||
self.contentWarning = cw
|
||||
} else {
|
||||
self.contentWarningEnabled = false
|
||||
self.contentWarning = ""
|
||||
}
|
||||
}
|
||||
self.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments)
|
||||
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
|
||||
self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility)
|
||||
|
||||
self.initialText = try container.decode(String.self, forKey: .initialText)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(lastModified, forKey: .lastModified)
|
||||
|
||||
try container.encode(accountID, forKey: .accountID)
|
||||
try container.encode(text, forKey: .text)
|
||||
try container.encode(contentWarningEnabled, forKey: .contentWarningEnabled)
|
||||
try container.encode(contentWarning, forKey: .contentWarning)
|
||||
try container.encode(attachments, forKey: .attachments)
|
||||
try container.encode(inReplyToID, forKey: .inReplyToID)
|
||||
try container.encode(visibility, forKey: .visibility)
|
||||
|
||||
try container.encode(initialText, forKey: .initialText)
|
||||
}
|
||||
}
|
||||
|
||||
extension Draft: Equatable {
|
||||
static func ==(lhs: Draft, rhs: Draft) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension Draft {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case lastModified
|
||||
|
||||
case accountID
|
||||
case text
|
||||
case contentWarningEnabled
|
||||
case contentWarning
|
||||
case attachments
|
||||
case inReplyToID
|
||||
case visibility
|
||||
|
||||
case initialText
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonController {
|
||||
|
||||
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil) -> Draft {
|
||||
var acctsToMention = [String]()
|
||||
|
||||
if let inReplyToID = inReplyToID,
|
||||
let inReplyTo = persistentContainer.status(for: inReplyToID) {
|
||||
acctsToMention.append(inReplyTo.account.acct)
|
||||
acctsToMention.append(contentsOf: inReplyTo.mentions.map(\.acct))
|
||||
}
|
||||
if let mentioningAcct = mentioningAcct {
|
||||
acctsToMention.append(mentioningAcct)
|
||||
}
|
||||
if let ownAccount = self.account {
|
||||
acctsToMention.removeAll(where: { $0 == ownAccount.acct })
|
||||
}
|
||||
acctsToMention = acctsToMention.uniques()
|
||||
|
||||
let draft = Draft(accountID: accountInfo!.id)
|
||||
draft.inReplyToID = inReplyToID
|
||||
draft.text = acctsToMention.map { "@\($0) " }.joined()
|
||||
draft.initialText = draft.text
|
||||
return draft
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// DraftsManager.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 10/22/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class DraftsManager: Codable {
|
||||
|
||||
private(set) static var shared: DraftsManager = load()
|
||||
|
||||
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
private static var archiveURL = DraftsManager.documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
|
||||
|
||||
static func save() {
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(shared)
|
||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||
}
|
||||
}
|
||||
|
||||
static func load() -> DraftsManager {
|
||||
let decoder = PropertyListDecoder()
|
||||
if let data = try? Data(contentsOf: archiveURL),
|
||||
let draftsManager = try? decoder.decode(DraftsManager.self, from: data) {
|
||||
return draftsManager
|
||||
}
|
||||
return DraftsManager()
|
||||
}
|
||||
|
||||
private init() {}
|
||||
|
||||
var drafts: [Draft] = []
|
||||
var sorted: [Draft] {
|
||||
return drafts.sorted(by: { $0.lastModified > $1.lastModified })
|
||||
}
|
||||
|
||||
func add(_ draft: Draft) {
|
||||
drafts.append(draft)
|
||||
}
|
||||
|
||||
func remove(_ draft: Draft) {
|
||||
drafts.removeAll { $0 == draft }
|
||||
}
|
||||
|
||||
}
|
|
@ -9,14 +9,14 @@
|
|||
import UIKit
|
||||
import Photos
|
||||
|
||||
protocol AssetPickerViewControllerDelegate {
|
||||
protocol AssetPickerViewControllerDelegate: class {
|
||||
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool
|
||||
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData])
|
||||
}
|
||||
|
||||
class AssetPickerViewController: UINavigationController {
|
||||
|
||||
var assetPickerDelegate: AssetPickerViewControllerDelegate?
|
||||
weak var assetPickerDelegate: AssetPickerViewControllerDelegate?
|
||||
|
||||
var currentCollectionSelectedAssets: [CompositionAttachmentData] {
|
||||
if let vc = visibleViewController as? AssetCollectionViewController {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// ComposeAssetPicker.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/19/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeAssetPicker: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = AssetPickerViewController
|
||||
|
||||
@ObservedObject var draft: Draft
|
||||
let delegate: AssetPickerViewControllerDelegate?
|
||||
|
||||
@EnvironmentObject var mastodonController: MastodonController
|
||||
|
||||
func makeUIViewController(context: Context) -> AssetPickerViewController {
|
||||
let vc = AssetPickerViewController()
|
||||
vc.assetPickerDelegate = delegate
|
||||
vc.preferredContentSize = CGSize(width: 400, height: 600)
|
||||
return vc
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AssetPickerViewController, context: Context) {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
//
|
||||
// ComposeAttachmentRow.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/19/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Photos
|
||||
import AVFoundation
|
||||
import Vision
|
||||
|
||||
struct ComposeAttachmentRow: View {
|
||||
@ObservedObject var draft: Draft
|
||||
@ObservedObject var attachment: CompositionAttachment
|
||||
let heightChanged: (CGFloat) -> Void
|
||||
|
||||
@EnvironmentObject var uiState: ComposeUIState
|
||||
@State private var mode: Mode = .allowEntry
|
||||
@State private var image: UIImage? = nil
|
||||
@State private var imageContentMode: ContentMode = .fill
|
||||
@State private var imageBackgroundColor: Color = .black
|
||||
@State private var isShowingTextRecognitionFailedAlert = false
|
||||
@State private var textRecognitionErrorMessage: String? = nil
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
imageView
|
||||
.frame(width: 80, height: 80)
|
||||
.cornerRadius(8)
|
||||
.contextMenu {
|
||||
if case .drawing(_) = attachment.data {
|
||||
Button(action: self.editDrawing) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Edit Drawing", systemImage: "hand.draw")
|
||||
} else {
|
||||
HStack {
|
||||
Text("Edit Drawing")
|
||||
Image(systemName: "hand.draw")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if attachment.data.type == .image {
|
||||
Button(action: self.recognizeText) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
||||
} else {
|
||||
HStack {
|
||||
Text("Recognize Text")
|
||||
Image(systemName: "doc.text.viewfinder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case .allowEntry:
|
||||
ComposeTextView(text: $attachment.attachmentDescription, placeholder: Text("Describe for the visually impaired…"), minHeight: 80)
|
||||
.heightDidChange(self.heightChanged)
|
||||
.backgroundColor(.clear)
|
||||
.fontSize(17)
|
||||
|
||||
case .recognizingText:
|
||||
if #available(iOS 14.0, *) {
|
||||
ProgressView()
|
||||
} else {
|
||||
ActivityIndicatorView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// todo: find a way to make this button not activated when the list row is selected, see FB8595628
|
||||
// Button(action: self.removeAttachment) {
|
||||
// Image(systemName: "xmark.circle.fill")
|
||||
// .foregroundColor(.blue)
|
||||
// }
|
||||
}
|
||||
.onAppear(perform: self.loadImage)
|
||||
.onReceive(attachment.$attachmentDescription) { (newDesc) in
|
||||
if newDesc.isEmpty {
|
||||
uiState.attachmentsMissingDescriptions.insert(attachment.id)
|
||||
} else {
|
||||
uiState.attachmentsMissingDescriptions.remove(attachment.id)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $isShowingTextRecognitionFailedAlert) {
|
||||
Alert(
|
||||
title: Text("Text Recognition Failed"),
|
||||
message: Text(self.textRecognitionErrorMessage ?? ""),
|
||||
dismissButton: .default(Text("OK"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var imageView: some View {
|
||||
if let image = image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: imageContentMode)
|
||||
.background(imageBackgroundColor)
|
||||
} else {
|
||||
Image(systemName: placeholderImageName)
|
||||
}
|
||||
}
|
||||
|
||||
private var placeholderImageName: String {
|
||||
switch colorScheme {
|
||||
case .dark:
|
||||
return "photo.fill"
|
||||
case .light:
|
||||
return "photo"
|
||||
@unknown default:
|
||||
return "photo"
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImage() {
|
||||
switch attachment.data {
|
||||
case let .image(image):
|
||||
self.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
|
||||
DispatchQueue.main.async {
|
||||
self.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) {
|
||||
self.image = UIImage(cgImage: cgImage)
|
||||
}
|
||||
case let .drawing(drawing):
|
||||
image = drawing.imageInLightMode(from: drawing.bounds)
|
||||
imageContentMode = .fit
|
||||
imageBackgroundColor = .white
|
||||
}
|
||||
}
|
||||
|
||||
private func removeAttachment() {
|
||||
draft.attachments.removeAll { $0.id == attachment.id }
|
||||
}
|
||||
|
||||
private func editDrawing() {
|
||||
uiState.composeDrawingMode = .edit(id: attachment.id)
|
||||
uiState.delegate?.presentComposeDrawing()
|
||||
}
|
||||
|
||||
private func recognizeText() {
|
||||
mode = .recognizingText
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.attachment.data.getData { (data, mimeType) in
|
||||
let handler = VNImageRequestHandler(data: data, options: [:])
|
||||
let request = VNRecognizeTextRequest { (request, error) in
|
||||
DispatchQueue.main.async {
|
||||
if let results = request.results as? [VNRecognizedTextObservation] {
|
||||
var text = ""
|
||||
for observation in results {
|
||||
let result = observation.topCandidates(1).first!
|
||||
text.append(result.string)
|
||||
text.append("\n")
|
||||
}
|
||||
self.attachment.attachmentDescription = text
|
||||
}
|
||||
|
||||
self.mode = .allowEntry
|
||||
}
|
||||
}
|
||||
request.recognitionLevel = .accurate
|
||||
request.usesLanguageCorrection = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch {
|
||||
// The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for.
|
||||
guard (error as NSError).code != 1 else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.mode = .allowEntry
|
||||
self.isShowingTextRecognitionFailedAlert = true
|
||||
self.textRecognitionErrorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeAttachmentRow {
|
||||
enum Mode {
|
||||
case allowEntry, recognizingText
|
||||
}
|
||||
}
|
||||
|
||||
//struct ComposeAttachmentRow_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ComposeAttachmentRow()
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,170 @@
|
|||
//
|
||||
// ComposeAttachmentsList.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/19/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeAttachmentsList: View {
|
||||
private let cellHeight: CGFloat = 80
|
||||
private let cellPadding: CGFloat = 12
|
||||
|
||||
@ObservedObject var draft: Draft
|
||||
|
||||
@EnvironmentObject var mastodonController: MastodonController
|
||||
@EnvironmentObject var uiState: ComposeUIState
|
||||
@State var isShowingAssetPickerPopover = false
|
||||
@State var isShowingCreateDrawing = false
|
||||
@State var rowHeights = [UUID: CGFloat]()
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(draft.attachments) { (attachment) in
|
||||
ComposeAttachmentRow(
|
||||
draft: draft,
|
||||
attachment: attachment
|
||||
) { (newHeight) in
|
||||
// in case height changed callback is called after atachment is removed but before view hierarchy is updated
|
||||
if draft.attachments.contains(where: { $0.id == attachment.id }) {
|
||||
rowHeights[attachment.id] = newHeight
|
||||
}
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||
}
|
||||
.onMove(perform: self.moveAttachments)
|
||||
.onDelete(perform: self.deleteAttachments)
|
||||
.conditionally(canAddAttachment) {
|
||||
$0.onInsert(of: CompositionAttachment.readableTypeIdentifiersForItemProvider, perform: self.insertAttachments)
|
||||
}
|
||||
|
||||
Button(action: self.addAttachment) {
|
||||
HStack {
|
||||
addButtonImage
|
||||
Text("Add image or video")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.blue)
|
||||
.disabled(!canAddAttachment)
|
||||
.frame(height: cellHeight)
|
||||
.popover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover)
|
||||
.contextMenu {
|
||||
Button(action: self.createDrawing) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Draw Something", systemImage: "hand.draw")
|
||||
} else {
|
||||
HStack {
|
||||
Text("Draw Something")
|
||||
Image(systemName: "hand.draw")
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(!canAddAttachment)
|
||||
}
|
||||
}
|
||||
.frame(height: totalListHeight)
|
||||
.onAppear(perform: self.didAppear)
|
||||
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
||||
}
|
||||
|
||||
private var addButtonImage: Image {
|
||||
let name: String
|
||||
switch colorScheme {
|
||||
case .dark:
|
||||
name = "photo.fill"
|
||||
case .light:
|
||||
name = "photo"
|
||||
@unknown default:
|
||||
name = "photo"
|
||||
}
|
||||
return Image(systemName: name)
|
||||
}
|
||||
|
||||
private var canAddAttachment: Bool {
|
||||
switch mastodonController.instance.instanceType {
|
||||
case .pleroma:
|
||||
return true
|
||||
case .mastodon:
|
||||
// todo: this technically allows invalid image/video combinations
|
||||
return draft.attachments.count < 4
|
||||
}
|
||||
}
|
||||
|
||||
private var totalListHeight: CGFloat {
|
||||
let totalRowHeights = rowHeights.values.reduce(0, +)
|
||||
let totalPadding = CGFloat(draft.attachments.count) * cellPadding
|
||||
let addButtonHeight = cellHeight + cellPadding
|
||||
return totalRowHeights + totalPadding + addButtonHeight
|
||||
}
|
||||
|
||||
private func didAppear() {
|
||||
let proxy = UITableView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
||||
// enable drag and drop to reorder on iPhone
|
||||
proxy.dragInteractionEnabled = true
|
||||
}
|
||||
|
||||
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
||||
var copy = rowHeights
|
||||
for k in copy.keys where !attachments.contains(where: { k == $0.id }) {
|
||||
copy.removeValue(forKey: k)
|
||||
}
|
||||
for attachment in attachments where !copy.keys.contains(attachment.id) {
|
||||
copy[attachment.id] = cellHeight
|
||||
}
|
||||
self.rowHeights = copy
|
||||
}
|
||||
|
||||
private func assetPickerPopover() -> some View {
|
||||
ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate)
|
||||
.onDisappear {
|
||||
self.isShowingAssetPickerPopover = false
|
||||
}
|
||||
.environment(\.colorScheme, .dark)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
}
|
||||
|
||||
private func addAttachment() {
|
||||
if horizontalSizeClass == .regular {
|
||||
isShowingAssetPickerPopover = true
|
||||
} else {
|
||||
uiState.delegate?.presentAssetPickerSheet()
|
||||
}
|
||||
}
|
||||
|
||||
private func moveAttachments(from source: IndexSet, to destination: Int) {
|
||||
draft.attachments.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
|
||||
private func deleteAttachments(at indices: IndexSet) {
|
||||
draft.attachments.remove(atOffsets: indices)
|
||||
}
|
||||
|
||||
private func insertAttachments(at offset: Int, itemProviders: [NSItemProvider]) {
|
||||
for provider in itemProviders where provider.canLoadObject(ofClass: CompositionAttachment.self) {
|
||||
guard canAddAttachment else { break }
|
||||
|
||||
provider.loadObject(ofClass: CompositionAttachment.self) { (object, error) in
|
||||
guard let attachment = object as? CompositionAttachment else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.draft.attachments.insert(attachment, at: offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createDrawing() {
|
||||
uiState.composeDrawingMode = .createNew
|
||||
uiState.delegate?.presentComposeDrawing()
|
||||
}
|
||||
}
|
||||
|
||||
//struct ComposeAttachmentsList_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ComposeAttachmentsList()
|
||||
// }
|
||||
//}
|
|
@ -1,563 +0,0 @@
|
|||
//
|
||||
// ComposeAttachmentsViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 3/11/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import MobileCoreServices
|
||||
import PencilKit
|
||||
import Photos
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private var currentlyEditedDrawingIndex: Int?
|
||||
|
||||
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
|
||||
|
||||
if mastodonController.instance == nil {
|
||||
mastodonController.getOwnInstance { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.updateAddAttachmentsButtonEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 nil:
|
||||
return false
|
||||
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 presentComposeDrawingViewController(editingAttachmentAt attachmentIndex: Int? = nil) {
|
||||
let drawingVC: ComposeDrawingViewController
|
||||
|
||||
if let index = attachmentIndex,
|
||||
case let .drawing(drawing) = attachments[index].data {
|
||||
drawingVC = ComposeDrawingViewController(editing: drawing)
|
||||
currentlyEditedDrawingIndex = index
|
||||
} else {
|
||||
drawingVC = ComposeDrawingViewController()
|
||||
}
|
||||
|
||||
drawingVC.delegate = self
|
||||
let nav = UINavigationController(rootViewController: drawingVC)
|
||||
nav.modalPresentationStyle = .fullScreen
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
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? {
|
||||
if indexPath.section == 0 {
|
||||
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
|
||||
var actions = [UIAction]()
|
||||
|
||||
switch attachment.data {
|
||||
case .drawing(_):
|
||||
actions.append(UIAction(title: "Edit Drawing", image: UIImage(systemName: "hand.draw"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
self.presentComposeDrawingViewController(editingAttachmentAt: indexPath.row)
|
||||
}))
|
||||
case .asset(_), .image(_):
|
||||
if attachment.data.type == .image,
|
||||
let cell = tableView.cellForRow(at: indexPath) as? ComposeAttachmentTableViewCell {
|
||||
|
||||
let title = NSLocalizedString("Recognize Text", comment: "recognize image attachment text menu item title")
|
||||
actions.append(UIAction(title: title, image: UIImage(systemName: "doc.text.viewfinder"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
cell.recognizeTextFromImage()
|
||||
}))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if actions.isEmpty {
|
||||
return nil
|
||||
} else {
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
||||
}
|
||||
}
|
||||
} else if indexPath.section == 1 {
|
||||
guard isAddAttachmentsButtonEnabled() else {
|
||||
return nil
|
||||
}
|
||||
// show context menu for drawing/file uploads
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
||||
UIAction(title: "Draw Something", image: UIImage(systemName: "hand.draw"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
self.presentComposeDrawingViewController()
|
||||
})
|
||||
])
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func targetedPreview(forConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
|
||||
indexPath.section == 0,
|
||||
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() {
|
||||
PHPhotoLibrary.requestAuthorization { (status) in
|
||||
guard status == .authorized else { return }
|
||||
DispatchQueue.main.async {
|
||||
if self.traitCollection.horizontalSizeClass == .compact {
|
||||
let sheetContainer = AssetPickerSheetContainerViewController()
|
||||
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||
self.present(sheetContainer, animated: true)
|
||||
} else {
|
||||
let picker = AssetPickerViewController()
|
||||
picker.assetPickerDelegate = self
|
||||
picker.overrideUserInterfaceStyle = .dark
|
||||
picker.modalPresentationStyle = .popover
|
||||
self.present(picker, animated: true)
|
||||
if let presentationController = picker.presentationController as? UIPopoverPresentationController {
|
||||
presentationController.sourceView = self.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 composeAttachment(_ cell: ComposeAttachmentTableViewCell, present viewController: UIViewController, animated: Bool) {
|
||||
self.present(viewController, animated: animated)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func composeAttachmentDescriptionHeightChanged(_ cell: ComposeAttachmentTableViewCell) {
|
||||
tableView.performBatchUpdates(nil) { (_) in
|
||||
self.updateHeightConstraint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeAttachmentsViewController: ComposeDrawingViewControllerDelegate {
|
||||
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) {
|
||||
dismiss(animated: true)
|
||||
currentlyEditedDrawingIndex = nil
|
||||
}
|
||||
|
||||
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) {
|
||||
let newAttachment = CompositionAttachment(data: .drawing(drawing))
|
||||
|
||||
if let currentlyEditedDrawingIndex = currentlyEditedDrawingIndex {
|
||||
attachments[currentlyEditedDrawingIndex] = newAttachment
|
||||
tableView.reloadRows(at: [IndexPath(row: currentlyEditedDrawingIndex, section: 0)], with: .automatic)
|
||||
} else {
|
||||
attachments.append(newAttachment)
|
||||
tableView.insertRows(at: [IndexPath(row: self.attachments.count - 1, section: 0)], with: .automatic)
|
||||
updateHeightConstraint()
|
||||
}
|
||||
|
||||
dismiss(animated: true)
|
||||
currentlyEditedDrawingIndex = nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// ComposeAvatarImageView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/18/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeAvatarImageView: View {
|
||||
let url: URL
|
||||
@State var request: ImageCache.Request? = nil
|
||||
@State var avatarImage: UIImage? = nil
|
||||
@ObservedObject var preferences = Preferences.shared
|
||||
|
||||
var body: some View {
|
||||
image
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
||||
.onAppear(perform: self.loadImage)
|
||||
.onDisappear(perform: self.cancelRequest)
|
||||
}
|
||||
|
||||
private var image: Image {
|
||||
if let avatarImage = avatarImage {
|
||||
return Image(uiImage: avatarImage)
|
||||
} else {
|
||||
let imageName: String
|
||||
switch preferences.avatarStyle {
|
||||
case .circle:
|
||||
imageName = "person.crop.circle"
|
||||
case .roundRect:
|
||||
imageName = "person.crop.square"
|
||||
}
|
||||
return Image(systemName: imageName)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImage() {
|
||||
request = ImageCache.avatars.get(url) { (data) in
|
||||
DispatchQueue.main.async {
|
||||
self.request = nil
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
self.avatarImage = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelRequest() {
|
||||
request?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeAvatarImageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ComposeAvatarImageView(url: URL(string: "https://social.shadowfacts.net/media/4b481afc591a8f3d11d0f5732e5cb320422dec72d7f223ebb5f35d5d0e821a9c.png")!)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// ComposeContainerView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/24/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct ComposeContainerView: View {
|
||||
let mastodonController: MastodonController
|
||||
let vcWidthSubject: PassthroughSubject<CGFloat, Never>
|
||||
@ObservedObject var uiState: ComposeUIState
|
||||
|
||||
init(
|
||||
mastodonController: MastodonController,
|
||||
vcWidthSubject: PassthroughSubject<CGFloat, Never>,
|
||||
uiState: ComposeUIState
|
||||
) {
|
||||
self.mastodonController = mastodonController
|
||||
self.vcWidthSubject = vcWidthSubject
|
||||
self.uiState = uiState
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ComposeView(
|
||||
draft: uiState.draft,
|
||||
vcWidthSubject: vcWidthSubject
|
||||
)
|
||||
.environmentObject(mastodonController)
|
||||
.environmentObject(uiState)
|
||||
}
|
||||
}
|
||||
|
||||
//struct ComposeContainerView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ComposeContainerView()
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// ComposeCurrentAccount.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/18/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Pachyderm
|
||||
|
||||
struct ComposeCurrentAccount: View {
|
||||
@EnvironmentObject var mastodonController: MastodonController
|
||||
|
||||
var account: Account {
|
||||
mastodonController.account!
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
ComposeAvatarImageView(url: account.avatar)
|
||||
VStack(alignment: .leading) {
|
||||
Text(verbatim: account.displayName)
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
|
||||
Text(verbatim: "@\(account.acct)")
|
||||
.font(.system(size: 17, weight: .light))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct ComposeCurrentAccount_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ComposeCurrentAccount(account: )
|
||||
// }
|
||||
//}
|
|
@ -175,9 +175,3 @@ extension ComposeDrawingViewController: PKToolPickerObserver {
|
|||
updateLayout(for: toolPicker)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeDrawingViewController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return otherGestureRecognizer == canvasView.drawingGestureRecognizer
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,370 @@
|
|||
//
|
||||
// ComposeHostingController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/22/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import Pachyderm
|
||||
import PencilKit
|
||||
|
||||
class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||
|
||||
let mastodonController: MastodonController
|
||||
|
||||
let uiState: ComposeUIState
|
||||
// storing the width in the UI state and having SwiftUI listen to it via @ObservedObject doesn't work
|
||||
// it ends up spinning forever
|
||||
let widthSubject = PassthroughSubject<CGFloat, Never>()
|
||||
|
||||
var draft: Draft { uiState.draft }
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
private var mainToolbar: UIToolbar!
|
||||
private var inputAccessoryToolbar: UIToolbar!
|
||||
private var visibilityBarButtonItems = [UIBarButtonItem]()
|
||||
|
||||
override var inputAccessoryView: UIView? { inputAccessoryToolbar }
|
||||
|
||||
init(draft: Draft? = nil, mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
let realDraft = draft ?? Draft(accountID: mastodonController.accountInfo!.id)
|
||||
DraftsManager.shared.add(realDraft)
|
||||
|
||||
self.uiState = ComposeUIState(draft: realDraft)
|
||||
|
||||
// we need our own environment object wrapper so that we can set the mastodon controller as an
|
||||
// environment object and setup the draft change listener while still having a concrete type
|
||||
// to use as the UIHostingController type parameter
|
||||
let container = ComposeContainerView(
|
||||
mastodonController: mastodonController,
|
||||
vcWidthSubject: widthSubject,
|
||||
uiState: uiState
|
||||
)
|
||||
super.init(rootView: container)
|
||||
|
||||
self.uiState.delegate = self
|
||||
|
||||
// main toolbar is shown at the bottom of the screen, the input accessory is attached to the keyboard while editing
|
||||
mainToolbar = createToolbar()
|
||||
inputAccessoryToolbar = createToolbar()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
|
||||
|
||||
// add the height of the toolbar itself to the bottom of the safe area so content inside SwiftUI ScrollView doesn't underflow it
|
||||
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 44, right: 0)
|
||||
|
||||
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
||||
|
||||
userActivity = UserActivityManager.newPostActivity()
|
||||
|
||||
self.uiState.$draft
|
||||
.flatMap(\.$visibility)
|
||||
.sink(receiveValue: self.visibilityChanged)
|
||||
.store(in: &cancellables)
|
||||
|
||||
self.uiState.$draft
|
||||
.flatMap(\.objectWillChange)
|
||||
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.global(qos: .utility))
|
||||
.sink {
|
||||
DraftsManager.save()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
widthSubject.send(view.bounds.width)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// can't do this in viewDidLoad because viewDidLoad isn't called for UIHostingController
|
||||
if mainToolbar.superview == nil {
|
||||
view.addSubview(mainToolbar)
|
||||
NSLayoutConstraint.activate([
|
||||
mainToolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
mainToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
// use the top anchor of the toolbar so our additionalSafeAreaInsets (which has the bottom as the toolbar height) don't affect it
|
||||
mainToolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if !draft.hasContent {
|
||||
DraftsManager.shared.remove(draft)
|
||||
}
|
||||
DraftsManager.save()
|
||||
}
|
||||
|
||||
private func createToolbar() -> UIToolbar {
|
||||
let toolbar = UIToolbar()
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: self, action: #selector(visibilityButtonPressed))
|
||||
visibilityItem.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||
visibilityBarButtonItems.append(visibilityItem)
|
||||
|
||||
toolbar.items = [
|
||||
UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)),
|
||||
visibilityItem,
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPresed))
|
||||
]
|
||||
return toolbar
|
||||
}
|
||||
|
||||
|
||||
@objc private func keyboardWillShow(_ notification: Foundation.Notification) {
|
||||
keyboardWillShow(accessoryView: inputAccessoryToolbar, notification: notification)
|
||||
}
|
||||
|
||||
func keyboardWillShow(accessoryView: UIView, notification: Foundation.Notification) {
|
||||
mainToolbar.isHidden = true
|
||||
|
||||
accessoryView.alpha = 1
|
||||
accessoryView.isHidden = false
|
||||
}
|
||||
|
||||
@objc private func keyboardWillHide(_ notification: Foundation.Notification) {
|
||||
keyboardWillHide(accessoryView: inputAccessoryToolbar, notification: notification)
|
||||
}
|
||||
|
||||
func keyboardWillHide(accessoryView: UIView, notification: Foundation.Notification) {
|
||||
mainToolbar.isHidden = false
|
||||
|
||||
let userInfo = notification.userInfo!
|
||||
let durationObj = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber
|
||||
let duration = TimeInterval(durationObj.doubleValue)
|
||||
let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! NSNumber
|
||||
let curve = UIView.AnimationCurve(rawValue: curveValue.intValue)!
|
||||
let curveOption: UIView.AnimationOptions
|
||||
switch curve {
|
||||
case .easeInOut:
|
||||
curveOption = .curveEaseInOut
|
||||
case .easeIn:
|
||||
curveOption = .curveEaseIn
|
||||
case .easeOut:
|
||||
curveOption = .curveEaseOut
|
||||
case .linear:
|
||||
curveOption = .curveLinear
|
||||
@unknown default:
|
||||
curveOption = .curveLinear
|
||||
}
|
||||
UIView.animate(withDuration: duration, delay: 0, options: curveOption) {
|
||||
accessoryView.alpha = 0
|
||||
} completion: { (finished) in
|
||||
accessoryView.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func keyboardDidHide(_ notification: Foundation.Notification) {
|
||||
keyboardDidHide(accessoryView: inputAccessoryToolbar, notification: notification)
|
||||
}
|
||||
|
||||
func keyboardDidHide(accessoryView: UIView, notification: Foundation.Notification) {
|
||||
accessoryView.isHidden = true
|
||||
}
|
||||
|
||||
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
||||
for item in visibilityBarButtonItems {
|
||||
item.image = UIImage(systemName: newVisibility.imageName)
|
||||
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
// todo: this technically allows invalid video/image combinations
|
||||
return itemProviders.count + draft.attachments.count <= 4
|
||||
}
|
||||
}
|
||||
|
||||
override func paste(itemProviders: [NSItemProvider]) {
|
||||
for provider in itemProviders where provider.canLoadObject(ofClass: CompositionAttachment.self) {
|
||||
provider.loadObject(ofClass: CompositionAttachment.self) { (object, error) in
|
||||
guard let attachment = object as? CompositionAttachment else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.draft.attachments.append(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@objc func cwButtonPressed() {
|
||||
draft.contentWarningEnabled = !draft.contentWarningEnabled
|
||||
}
|
||||
|
||||
@objc func visibilityButtonPressed(_ sender: UIBarButtonItem) {
|
||||
let alertController = UIAlertController(currentVisibility: draft.visibility) { (visibility) in
|
||||
guard let visibility = visibility else { return }
|
||||
self.draft.visibility = visibility
|
||||
}
|
||||
alertController.popoverPresentationController?.barButtonItem = sender
|
||||
present(alertController, animated: true)
|
||||
}
|
||||
|
||||
@objc func draftsButtonPresed() {
|
||||
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
||||
draftsVC.delegate = self
|
||||
present(UINavigationController(rootViewController: draftsVC), animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeHostingController {
|
||||
struct EnvironmentWrappingView<Content: View, EnvironmentObject: ObservableObject>: View {
|
||||
let content: Content
|
||||
let environmentObject: EnvironmentObject
|
||||
|
||||
var body: some View {
|
||||
content.environmentObject(environmentObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeHostingController: ComposeUIStateDelegate {
|
||||
var assetPickerDelegate: AssetPickerViewControllerDelegate? { self }
|
||||
|
||||
func dismissCompose() {
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func presentAssetPickerSheet() {
|
||||
let sheetContainer = AssetPickerSheetContainerViewController()
|
||||
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||
self.present(sheetContainer, animated: true)
|
||||
}
|
||||
|
||||
func presentComposeDrawing() {
|
||||
let drawing: PKDrawing
|
||||
|
||||
if case let .edit(id) = uiState.composeDrawingMode,
|
||||
let attachment = draft.attachments.first(where: { $0.id == id }),
|
||||
case let .drawing(existingDrawing) = attachment.data {
|
||||
drawing = existingDrawing
|
||||
} else {
|
||||
drawing = PKDrawing()
|
||||
}
|
||||
|
||||
let drawingVC = ComposeDrawingViewController(editing: drawing)
|
||||
drawingVC.delegate = self
|
||||
let nav = UINavigationController(rootViewController: drawingVC)
|
||||
nav.modalPresentationStyle = .fullScreen
|
||||
present(nav, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeHostingController: AssetPickerViewControllerDelegate {
|
||||
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool {
|
||||
switch mastodonController.instance.instanceType {
|
||||
case .pleroma:
|
||||
return true
|
||||
case .mastodon:
|
||||
if (type == .video && draft.attachments.count > 0) ||
|
||||
draft.attachments.contains(where: { $0.data.type == .video }) ||
|
||||
assetPicker.currentCollectionSelectedAssets.contains(where: { $0.type == .video }) {
|
||||
return false
|
||||
}
|
||||
return draft.attachments.count + assetPicker.currentCollectionSelectedAssets.count < 4
|
||||
}
|
||||
}
|
||||
|
||||
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData]) {
|
||||
let attachments = attachments.map {
|
||||
CompositionAttachment(data: $0)
|
||||
}
|
||||
draft.attachments.append(contentsOf: attachments)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeHostingController: DraftsTableViewControllerDelegate {
|
||||
func draftSelectionCanceled() {
|
||||
}
|
||||
|
||||
func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void) {
|
||||
if draft.inReplyToID != self.draft.inReplyToID,
|
||||
self.draft.hasContent {
|
||||
let alertController = UIAlertController(title: "Different Reply", message: "The selected draft is a reply to a different status, do you wish to use it?", preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
|
||||
completion(false)
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: "Restore Draft", style: .default, handler: { (_) in
|
||||
completion(true)
|
||||
}))
|
||||
// we can't present the laert ourselves since the compose VC is already presenting the draft selector
|
||||
// but presenting on the presented view controller seems hacky, is there a better way to do this?
|
||||
presentedViewController!.present(alertController, animated: true)
|
||||
} else {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
func draftSelected(_ draft: Draft) {
|
||||
if self.draft.hasContent {
|
||||
DraftsManager.save()
|
||||
} else {
|
||||
DraftsManager.shared.remove(self.draft)
|
||||
}
|
||||
|
||||
uiState.draft = draft
|
||||
}
|
||||
|
||||
func draftSelectionCompleted() {
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeHostingController: UIAdaptivePresentationControllerDelegate {
|
||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||
return Preferences.shared.automaticallySaveDrafts || !draft.hasContent
|
||||
}
|
||||
|
||||
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
|
||||
uiState.isShowingSaveDraftSheet = true
|
||||
}
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
DraftsManager.save()
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
|
||||
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) {
|
||||
switch uiState.composeDrawingMode {
|
||||
case nil, .createNew:
|
||||
let attachment = CompositionAttachment(data: .drawing(drawing))
|
||||
draft.attachments.append(attachment)
|
||||
|
||||
case let .edit(id):
|
||||
let existing = draft.attachments.first { $0.id == id }
|
||||
existing?.data = .drawing(drawing)
|
||||
}
|
||||
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// ComposeHostingController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/18/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
class ComposeHostingController: UIHostingController<ComposeView> {
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// ComposeReplyContentView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/22/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeReplyContentView: UIViewRepresentable {
|
||||
typealias UIViewType = ComposeReplyContentTextView
|
||||
|
||||
let status: StatusMO
|
||||
let maxWidth: CGFloat
|
||||
|
||||
@EnvironmentObject var mastodonController: MastodonController
|
||||
|
||||
func makeUIView(context: Context) -> ComposeReplyContentTextView {
|
||||
let view = ComposeReplyContentTextView()
|
||||
view.overrideMastodonController = mastodonController
|
||||
view.setTextFrom(status: status)
|
||||
view.isScrollEnabled = false
|
||||
view.isUserInteractionEnabled = false
|
||||
view.backgroundColor = .clear
|
||||
view.maxWidth = maxWidth
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: ComposeReplyContentTextView, context: Context) {
|
||||
uiView.constraint.constant = maxWidth
|
||||
}
|
||||
}
|
||||
|
||||
class ComposeReplyContentTextView: StatusContentTextView {
|
||||
|
||||
var maxWidth: CGFloat!
|
||||
var constraint: NSLayoutConstraint!
|
||||
|
||||
override func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
constraint = widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth)
|
||||
constraint.isActive = true
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// ComposeReplyView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/22/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeReplyView: View {
|
||||
let status: StatusMO
|
||||
let maxWidth: CGFloat
|
||||
let stackPadding: CGFloat
|
||||
|
||||
private let horizSpacing: CGFloat = 8
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: horizSpacing) {
|
||||
GeometryReader(content: self.replyAvatarImage)
|
||||
.frame(width: 50)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
Text(verbatim: status.account.displayName)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
|
||||
Text(verbatim: "@\(status.account.acct)")
|
||||
.font(.system(size: 17, weight: .light))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
ComposeReplyContentView(status: status, maxWidth: maxWidth - 50 - horizSpacing + 4)
|
||||
.offset(x: -4, y: -8)
|
||||
.padding(.bottom, -8)
|
||||
}
|
||||
.frame(minHeight: 50 + 8)
|
||||
}
|
||||
.padding(.bottom, -8)
|
||||
}
|
||||
|
||||
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
||||
let scrollOffset = geometry.frame(in: .named("outer")).minY - stackPadding
|
||||
let offset = min(max(-scrollOffset, 0), geometry.size.height - 50 - 8)
|
||||
return ComposeAvatarImageView(url: status.account.avatar)
|
||||
.offset(x: 0, y: offset)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//struct ComposeReplyView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ComposeReplyView()
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// ComposeTextView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/18/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeTextView: View {
|
||||
@Binding private var text: String
|
||||
private let placeholder: Text?
|
||||
private let minHeight: CGFloat
|
||||
|
||||
private var heightDidChange: ((CGFloat) -> Void)?
|
||||
private var backgroundColor = UIColor.secondarySystemBackground
|
||||
private var fontSize: CGFloat = 20
|
||||
|
||||
@State private var height: CGFloat?
|
||||
|
||||
init(text: Binding<String>, placeholder: Text?, minHeight: CGFloat = 150) {
|
||||
self._text = text
|
||||
self.placeholder = placeholder
|
||||
self.minHeight = minHeight
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
WrappedTextView(
|
||||
text: $text,
|
||||
textDidChange: self.textDidChange,
|
||||
backgroundColor: backgroundColor,
|
||||
font: .systemFont(ofSize: fontSize)
|
||||
)
|
||||
.frame(height: height ?? minHeight)
|
||||
|
||||
if text.isEmpty, let placeholder = placeholder {
|
||||
placeholder
|
||||
.font(.system(size: fontSize))
|
||||
.foregroundColor(.secondary)
|
||||
.offset(x: 4, y: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func textDidChange(textView: UITextView) {
|
||||
height = max(minHeight, textView.contentSize.height)
|
||||
heightDidChange?(height!)
|
||||
}
|
||||
|
||||
func heightDidChange(_ callback: @escaping (CGFloat) -> Void) -> Self {
|
||||
var copy = self
|
||||
copy.heightDidChange = callback
|
||||
return copy
|
||||
}
|
||||
|
||||
func backgroundColor(_ color: UIColor) -> Self {
|
||||
var copy = self
|
||||
copy.backgroundColor = color
|
||||
return copy
|
||||
}
|
||||
|
||||
func fontSize(_ size: CGFloat) -> Self {
|
||||
var copy = self
|
||||
copy.fontSize = size
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
struct WrappedTextView: UIViewRepresentable {
|
||||
typealias UIViewType = UITextView
|
||||
|
||||
@Binding var text: String
|
||||
var textDidChange: ((UITextView) -> Void)?
|
||||
var backgroundColor = UIColor.secondarySystemBackground
|
||||
var font = UIFont.systemFont(ofSize: 20)
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let textView = UITextView()
|
||||
textView.delegate = context.coordinator
|
||||
textView.isEditable = true
|
||||
textView.backgroundColor = backgroundColor
|
||||
textView.font = font
|
||||
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||
uiView.text = text
|
||||
context.coordinator.text = $text
|
||||
context.coordinator.didChange = textDidChange
|
||||
// wait until the next runloop iteration so that SwiftUI view updates have finished and
|
||||
// the text view knows its new content size
|
||||
DispatchQueue.main.async {
|
||||
self.textDidChange?(uiView)
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator(text: $text, didChange: textDidChange)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextViewDelegate {
|
||||
var text: Binding<String>
|
||||
var didChange: ((UITextView) -> Void)?
|
||||
|
||||
init(text: Binding<String>, didChange: ((UITextView) -> Void)?) {
|
||||
self.text = text
|
||||
self.didChange = didChange
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
text.wrappedValue = textView.text
|
||||
didChange?(textView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//struct ComposeTextView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ComposeTextView()
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// ComposeUIState.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/24/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
protocol ComposeUIStateDelegate: class {
|
||||
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
|
||||
|
||||
func dismissCompose()
|
||||
func presentAssetPickerSheet()
|
||||
func presentComposeDrawing()
|
||||
|
||||
func keyboardWillShow(accessoryView: UIView, notification: Notification)
|
||||
func keyboardWillHide(accessoryView: UIView, notification: Notification)
|
||||
func keyboardDidHide(accessoryView: UIView, notification: Notification)
|
||||
}
|
||||
|
||||
class ComposeUIState: ObservableObject {
|
||||
|
||||
weak var delegate: ComposeUIStateDelegate?
|
||||
|
||||
@Published var draft: Draft
|
||||
@Published var isShowingSaveDraftSheet = false
|
||||
@Published var attachmentsMissingDescriptions = Set<UUID>()
|
||||
|
||||
var composeDrawingMode: ComposeDrawingMode?
|
||||
|
||||
init(draft: Draft) {
|
||||
self.draft = draft
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeUIState {
|
||||
enum ComposeDrawingMode {
|
||||
case createNew
|
||||
case edit(id: UUID)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,292 @@
|
|||
//
|
||||
// ComposeView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/18/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Pachyderm
|
||||
import Combine
|
||||
|
||||
struct ComposeView: View {
|
||||
@ObservedObject var draft: Draft
|
||||
let vcWidthSubject: PassthroughSubject<CGFloat, Never>
|
||||
|
||||
@EnvironmentObject var mastodonController: MastodonController
|
||||
@State var viewControllerWidth: CGFloat = 0
|
||||
@EnvironmentObject var uiState: ComposeUIState
|
||||
@State var isPosting = false
|
||||
@State var postProgress: Double = 0
|
||||
@State var postTotalProgress: Double = 0
|
||||
@State var isShowingPostErrorAlert = false
|
||||
@State var postError: Error?
|
||||
|
||||
private let stackPadding: CGFloat = 8
|
||||
|
||||
init(
|
||||
draft: Draft,
|
||||
vcWidthSubject: PassthroughSubject<CGFloat, Never>
|
||||
) {
|
||||
self.draft = draft
|
||||
self.vcWidthSubject = vcWidthSubject
|
||||
}
|
||||
|
||||
var charactersRemaining: Int {
|
||||
let limit = mastodonController.instance.maxStatusCharacters ?? 500
|
||||
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
|
||||
return limit - (cwCount + CharacterCounter.count(text: draft.text))
|
||||
}
|
||||
|
||||
var requiresAttachmentDescriptions: Bool {
|
||||
guard Preferences.shared.requireAttachmentDescriptions else { return false }
|
||||
let attachmentIds = draft.attachments.map(\.id)
|
||||
return attachmentIds.contains { uiState.attachmentsMissingDescriptions.contains($0) }
|
||||
}
|
||||
|
||||
var postButtonEnabled: Bool {
|
||||
draft.hasContent && charactersRemaining >= 0 && !isPosting && !requiresAttachmentDescriptions
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// the pre-iOS 14 API does not result in the correct pointer interactions for nav bar buttons, see FB8595468
|
||||
if #available(iOS 14.0, *) {
|
||||
mostOfTheBody.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { cancelButton }
|
||||
ToolbarItem(placement: .confirmationAction) { postButton }
|
||||
}
|
||||
} else {
|
||||
mostOfTheBody.navigationBarItems(leading: cancelButton, trailing: postButton)
|
||||
}
|
||||
}
|
||||
|
||||
var mostOfTheBody: some View {
|
||||
ZStack(alignment: .top) {
|
||||
ScrollView(.vertical) {
|
||||
mainStack
|
||||
}
|
||||
|
||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
||||
}
|
||||
.coordinateSpace(name: "outer")
|
||||
.onAppear(perform: self.didAppear)
|
||||
.navigationBarTitle("Compose")
|
||||
.onReceive(vcWidthSubject) { self.viewControllerWidth = $0 }
|
||||
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
|
||||
.alert(isPresented: $isShowingPostErrorAlert) {
|
||||
Alert(
|
||||
title: Text("Error Posting Status"),
|
||||
message: Text(postError?.localizedDescription ?? ""),
|
||||
dismissButton: .default(Text("OK"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var mainStack: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let id = draft.inReplyToID,
|
||||
let status = mastodonController.persistentContainer.status(for: id) {
|
||||
ComposeReplyView(
|
||||
status: status,
|
||||
maxWidth: viewControllerWidth - (2 * stackPadding),
|
||||
stackPadding: stackPadding
|
||||
)
|
||||
}
|
||||
|
||||
header
|
||||
|
||||
if draft.contentWarningEnabled {
|
||||
TextField("Write your warning here", text: $draft.contentWarning)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
}
|
||||
|
||||
MainComposeTextView(
|
||||
draft: draft,
|
||||
placeholder: Text("What's on your mind?")
|
||||
)
|
||||
|
||||
ComposeAttachmentsList(
|
||||
draft: draft
|
||||
)
|
||||
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack
|
||||
.padding([.top, .bottom], -8)
|
||||
}
|
||||
.padding(stackPadding)
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(alignment: .top) {
|
||||
ComposeCurrentAccount()
|
||||
Spacer()
|
||||
Text(verbatim: charactersRemaining.description)
|
||||
.foregroundColor(charactersRemaining < 0 ? .red : .secondary)
|
||||
.font(Font.body.monospacedDigit())
|
||||
}.frame(height: 50)
|
||||
}
|
||||
|
||||
private var cancelButton: some View {
|
||||
Button(action: self.cancel) {
|
||||
Text("Cancel")
|
||||
// otherwise all Buttons in the nav bar are made semibold
|
||||
.font(.system(size: 17, weight: .regular))
|
||||
}
|
||||
}
|
||||
|
||||
private var postButton: some View {
|
||||
Button(action: self.postStatus) {
|
||||
Text("Post")
|
||||
}
|
||||
.disabled(!postButtonEnabled)
|
||||
}
|
||||
|
||||
private func didAppear() {
|
||||
let proxy = UIScrollView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
||||
proxy.keyboardDismissMode = .interactive
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
if Preferences.shared.automaticallySaveDrafts {
|
||||
// draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear
|
||||
uiState.delegate?.dismissCompose()
|
||||
} else {
|
||||
uiState.isShowingSaveDraftSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
private func saveAndCloseSheet() -> ActionSheet {
|
||||
ActionSheet(title: Text("Do you want to save the current post as a draft?"), buttons: [
|
||||
.default(Text("Save Draft"), action: {
|
||||
// draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear
|
||||
uiState.isShowingSaveDraftSheet = false
|
||||
uiState.delegate?.dismissCompose()
|
||||
}),
|
||||
.destructive(Text("Delete Draft"), action: {
|
||||
DraftsManager.shared.remove(draft)
|
||||
uiState.isShowingSaveDraftSheet = false
|
||||
uiState.delegate?.dismissCompose()
|
||||
}),
|
||||
.cancel(),
|
||||
])
|
||||
}
|
||||
|
||||
private func postStatus() {
|
||||
guard draft.hasContent else { return }
|
||||
|
||||
isPosting = true
|
||||
|
||||
// save before posting, so if a crash occurs during network request, the status won't be lost
|
||||
DraftsManager.save()
|
||||
|
||||
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : nil
|
||||
let sensitive = contentWarning != nil
|
||||
|
||||
// 2 steps (request data, then upload) for each attachment
|
||||
postTotalProgress = Double(2 + (draft.attachments.count * 2))
|
||||
postProgress = 1
|
||||
|
||||
uploadAttachments { (result) in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
self.isShowingPostErrorAlert = true
|
||||
self.postError = error
|
||||
|
||||
case let .success(uploadedAttachments):
|
||||
let request = Client.createStatus(text: draft.text,
|
||||
contentType: Preferences.shared.statusContentType,
|
||||
inReplyTo: draft.inReplyToID,
|
||||
media: uploadedAttachments,
|
||||
sensitive: sensitive,
|
||||
spoilerText: contentWarning,
|
||||
visibility: draft.visibility,
|
||||
language: nil)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
self.isShowingPostErrorAlert = true
|
||||
self.postError = error
|
||||
|
||||
case .success(_, _):
|
||||
self.postProgress += 1
|
||||
|
||||
DraftsManager.shared.remove(self.draft)
|
||||
|
||||
// wait .25 seconds so the user can see the progress bar has completed
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
||||
self.uiState.delegate?.dismissCompose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], Error>) -> Void) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
var anyFailed = false
|
||||
var uploadedAttachments = [Result<Attachment, Error>?]()
|
||||
|
||||
for (index, compAttachment) in draft.attachments.enumerated() {
|
||||
group.enter()
|
||||
|
||||
uploadedAttachments.append(nil)
|
||||
|
||||
compAttachment.data.getData { (data, mimeType) in
|
||||
postProgress += 1
|
||||
|
||||
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
||||
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
postProgress += 1
|
||||
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
uploadedAttachments[index] = .failure(error)
|
||||
anyFailed = true
|
||||
case let .success(attachment, _):
|
||||
uploadedAttachments[index] = .success(attachment)
|
||||
}
|
||||
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
if anyFailed {
|
||||
let errors = uploadedAttachments.map { (result) -> Error? in
|
||||
if case let .failure(error) = result {
|
||||
return error
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
completion(.failure(AttachmentUploadError(errors: errors)))
|
||||
} else {
|
||||
let uploadedAttachments = uploadedAttachments.map {
|
||||
try! $0!.get()
|
||||
}
|
||||
completion(.success(uploadedAttachments))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct AttachmentUploadError: LocalizedError {
|
||||
let errors: [Error?]
|
||||
|
||||
var localizedDescription: String {
|
||||
return errors.enumerated().compactMap { (index, error) -> String? in
|
||||
guard let error = error else { return nil }
|
||||
return "Attachment \(index + 1): \(error.localizedDescription)"
|
||||
}.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
//struct ComposeView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ComposeView()
|
||||
// }
|
||||
//}
|
|
@ -1,619 +0,0 @@
|
|||
//
|
||||
// ComposeViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/28/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import Intents
|
||||
|
||||
class ComposeViewController: UIViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
var inReplyToID: String?
|
||||
var accountsToMention = [String]()
|
||||
var initialText: String?
|
||||
var contentWarningEnabled = false {
|
||||
didSet {
|
||||
contentWarningStateChanged()
|
||||
}
|
||||
}
|
||||
var visibility: Status.Visibility! {
|
||||
didSet {
|
||||
visibilityChanged()
|
||||
}
|
||||
}
|
||||
|
||||
var hasChanges = false
|
||||
var currentDraft: DraftsManager.Draft?
|
||||
// Weak so that if a new session is initiated (i.e. XCBManager.currentSession is changed) while the current one is in progress, this one will be released
|
||||
weak var xcbSession: XCBSession?
|
||||
var postedStatus: Status?
|
||||
|
||||
var compositionState: CompositionState = .valid {
|
||||
didSet {
|
||||
postBarButtonItem.isEnabled = compositionState.isValid
|
||||
}
|
||||
}
|
||||
|
||||
weak var postBarButtonItem: UIBarButtonItem!
|
||||
var visibilityBarButtonItem: UIBarButtonItem!
|
||||
var contentWarningBarButtonItem: UIBarButtonItem!
|
||||
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
@IBOutlet weak var contentView: UIView!
|
||||
@IBOutlet weak var stackView: UIStackView!
|
||||
|
||||
var replyView: ComposeStatusReplyView?
|
||||
var replyAvatarImageViewTopConstraint: NSLayoutConstraint?
|
||||
|
||||
@IBOutlet weak var selfDetailView: LargeAccountDetailView!
|
||||
|
||||
@IBOutlet weak var charactersRemainingLabel: UILabel!
|
||||
@IBOutlet weak var statusTextView: UITextView!
|
||||
@IBOutlet weak var placeholderLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var inReplyToContainer: UIView!
|
||||
@IBOutlet weak var inReplyToLabel: UILabel!
|
||||
@IBOutlet weak var contentWarningContainerView: UIView!
|
||||
@IBOutlet weak var contentWarningTextField: UITextField!
|
||||
|
||||
@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
|
||||
|
||||
self.inReplyToID = inReplyToID
|
||||
if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.persistentContainer.status(for: inReplyToID) {
|
||||
accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct }
|
||||
} else {
|
||||
accountsToMention = []
|
||||
}
|
||||
if let mentioningAcct = mentioningAcct {
|
||||
accountsToMention.append(mentioningAcct)
|
||||
}
|
||||
if let ownAccount = mastodonController.account {
|
||||
accountsToMention.removeAll(where: { acct in ownAccount.acct == acct })
|
||||
}
|
||||
accountsToMention = accountsToMention.uniques()
|
||||
|
||||
super.init(nibName: "ComposeViewController", bundle: nil)
|
||||
|
||||
title = "Compose"
|
||||
tabBarItem.image = UIImage(systemName: "pencil")
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(showSaveAndClosePrompt))
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Post", style: .done, target: self, action: #selector(postButtonPressed))
|
||||
postBarButtonItem = navigationItem.rightBarButtonItem
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
scrollView.delegate = self
|
||||
|
||||
statusTextView.delegate = self
|
||||
statusTextView.becomeFirstResponder()
|
||||
|
||||
let toolbar = UIToolbar()
|
||||
contentWarningBarButtonItem = UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(contentWarningButtonPressed))
|
||||
contentWarningBarButtonItem.accessibilityLabel = NSLocalizedString("Add Content Warning", comment: "add CW accessibility label")
|
||||
visibilityBarButtonItem = UIBarButtonItem(image: UIImage(systemName: Preferences.shared.defaultPostVisibility.imageName), style: .plain, target: self, action: #selector(visibilityButtonPressed))
|
||||
visibilityBarButtonItem.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), Preferences.shared.defaultPostVisibility.displayName)
|
||||
toolbar.items = [
|
||||
contentWarningBarButtonItem,
|
||||
visibilityBarButtonItem,
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
] + createFormattingButtons() + [
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPressed))
|
||||
]
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusTextView.inputAccessoryView = toolbar
|
||||
contentWarningTextField.inputAccessoryView = toolbar
|
||||
|
||||
statusTextView.text = accountsToMention.map({ acct in "@\(acct) " }).joined()
|
||||
initialText = statusTextView.text
|
||||
|
||||
mastodonController.getOwnAccount { (account) in
|
||||
DispatchQueue.main.async {
|
||||
self.selfDetailView.update(account: account)
|
||||
}
|
||||
}
|
||||
|
||||
updateInReplyTo()
|
||||
|
||||
// we have to set the font here, because the monospaced digit font is not available in IB
|
||||
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
|
||||
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)
|
||||
}
|
||||
|
||||
func updateInReplyTo() {
|
||||
if let replyView = replyView {
|
||||
replyView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if let inReplyToID = inReplyToID {
|
||||
if let status = mastodonController.persistentContainer.status(for: inReplyToID) {
|
||||
updateInReplyTo(inReplyTo: status)
|
||||
} else {
|
||||
let loadingVC = LoadingViewController()
|
||||
embedChild(loadingVC)
|
||||
|
||||
let request = Client.getStatus(id: inReplyToID)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(status, _) = response else { return }
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: true) { (status) in
|
||||
DispatchQueue.main.async {
|
||||
self.updateInReplyTo(inReplyTo: status)
|
||||
loadingVC.removeViewAndController()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
visibility = Preferences.shared.defaultPostVisibility
|
||||
contentWarningEnabled = false
|
||||
|
||||
inReplyToContainer.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateInReplyTo(inReplyTo: StatusMO) {
|
||||
visibility = inReplyTo.visibility
|
||||
if Preferences.shared.contentWarningCopyMode == .doNotCopy {
|
||||
contentWarningEnabled = false
|
||||
contentWarningContainerView.isHidden = true
|
||||
} else {
|
||||
contentWarningEnabled = !inReplyTo.spoilerText.isEmpty
|
||||
contentWarningContainerView.isHidden = !contentWarningEnabled
|
||||
if Preferences.shared.contentWarningCopyMode == .prependRe,
|
||||
!inReplyTo.spoilerText.lowercased().starts(with: "re:") {
|
||||
contentWarningTextField.text = "re: \(inReplyTo.spoilerText)"
|
||||
} else {
|
||||
contentWarningTextField.text = inReplyTo.spoilerText
|
||||
}
|
||||
}
|
||||
|
||||
let replyView = ComposeStatusReplyView.create()
|
||||
replyView.mastodonController = mastodonController
|
||||
replyView.updateUI(for: inReplyTo)
|
||||
stackView.insertArrangedSubview(replyView, at: 0)
|
||||
|
||||
self.replyView = replyView
|
||||
|
||||
replyAvatarImageViewTopConstraint = replyView.avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)
|
||||
replyAvatarImageViewTopConstraint!.isActive = true
|
||||
|
||||
inReplyToContainer.isHidden = false
|
||||
// todo: update to use managed objects
|
||||
inReplyToLabel.text = "In reply to \(inReplyTo.account.displayName)"
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
}
|
||||
|
||||
func createFormattingButtons() -> [UIBarButtonItem] {
|
||||
guard Preferences.shared.statusContentType != .plain else {
|
||||
return []
|
||||
}
|
||||
|
||||
var formatButtons = StatusFormat.allCases.map { (format) -> UIBarButtonItem in
|
||||
let item: UIBarButtonItem
|
||||
if let image = format.image {
|
||||
item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
|
||||
} else if let (str, attributes) = format.title {
|
||||
item = UIBarButtonItem(title: str, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
|
||||
item.setTitleTextAttributes(attributes, for: .normal)
|
||||
item.setTitleTextAttributes(attributes, for: .highlighted)
|
||||
} else {
|
||||
fatalError("StatusFormat must have either an image or a title")
|
||||
}
|
||||
item.tag = StatusFormat.allCases.firstIndex(of: format)!
|
||||
item.accessibilityLabel = format.accessibilityLabel
|
||||
return item
|
||||
}
|
||||
|
||||
for i in (1..<StatusFormat.allCases.count).reversed() {
|
||||
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
|
||||
spacer.width = 8
|
||||
formatButtons.insert(spacer, at: i)
|
||||
}
|
||||
|
||||
return formatButtons
|
||||
}
|
||||
|
||||
@objc func adjustForKeyboard(notification: NSNotification) {
|
||||
let userInfo = notification.userInfo!
|
||||
|
||||
let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
|
||||
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
|
||||
|
||||
if notification.name == UIResponder.keyboardWillHideNotification {
|
||||
scrollView.contentInset = .zero
|
||||
} else {
|
||||
// let accessoryFrame = view.convert(statusTextView.inputAccessoryView!.frame, from: view.window)
|
||||
let offset = keyboardViewEndFrame.height// + accessoryFrame.height
|
||||
// TODO: radar for incorrect keyboard end frame height (either converted or screen)
|
||||
// the value returned is somewhere between the height of the keyboard and the height of the keyboard + accessory
|
||||
// actually maybe not??
|
||||
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: offset, right: 0)
|
||||
}
|
||||
scrollView.scrollIndicatorInsets = scrollView.contentInset
|
||||
}
|
||||
|
||||
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
|
||||
if remaining < 0 {
|
||||
charactersRemainingLabel.textColor = .red
|
||||
compositionState.formUnion(.tooManyCharacters)
|
||||
} else {
|
||||
charactersRemainingLabel.textColor = .darkGray
|
||||
compositionState.subtract(.tooManyCharacters)
|
||||
}
|
||||
charactersRemainingLabel.text = String(remaining)
|
||||
charactersRemainingLabel.accessibilityLabel = String(format: NSLocalizedString("%d characters remaining", comment: "compose characters remaining accessibility label"), remaining)
|
||||
}
|
||||
|
||||
func updateHasChanges() {
|
||||
if let currentDraft = currentDraft {
|
||||
let cw = contentWarningEnabled ? contentWarningTextField.text : nil
|
||||
hasChanges = statusTextView.text != currentDraft.text || cw != currentDraft.contentWarning
|
||||
} else {
|
||||
hasChanges = !statusTextView.text.isEmpty || (contentWarningEnabled && !(contentWarningTextField.text?.isEmpty ?? true))
|
||||
}
|
||||
}
|
||||
|
||||
func updatePlaceholder() {
|
||||
placeholderLabel.isHidden = !statusTextView.text.isEmpty
|
||||
}
|
||||
|
||||
func contentWarningStateChanged() {
|
||||
contentWarningContainerView.isHidden = !contentWarningEnabled
|
||||
if contentWarningEnabled {
|
||||
contentWarningBarButtonItem.accessibilityLabel = NSLocalizedString("Remove Content Warning", comment: "remove CW accessibility label")
|
||||
} else {
|
||||
contentWarningBarButtonItem.accessibilityLabel = NSLocalizedString("Add Content Warning", comment: "add CW accessibility label")
|
||||
}
|
||||
}
|
||||
|
||||
func visibilityChanged() {
|
||||
visibilityBarButtonItem.image = UIImage(systemName: visibility.imageName)
|
||||
visibilityBarButtonItem.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), Preferences.shared.defaultPostVisibility.displayName)
|
||||
}
|
||||
|
||||
func saveDraft() {
|
||||
let attachments = composeAttachmentsViewController.attachments
|
||||
let statusText = statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let cw = contentWarningEnabled ? contentWarningTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) : nil
|
||||
let account = mastodonController.accountInfo!
|
||||
if attachments.count == 0, statusText.isEmpty, cw?.isEmpty ?? true {
|
||||
if let currentDraft = self.currentDraft {
|
||||
DraftsManager.shared.remove(currentDraft)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if let currentDraft = self.currentDraft {
|
||||
currentDraft.update(accountID: account.id, text: statusText, contentWarning: cw, attachments: attachments)
|
||||
} else {
|
||||
self.currentDraft = DraftsManager.shared.create(accountID: account.id, text: statusText, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments)
|
||||
}
|
||||
}
|
||||
DraftsManager.save()
|
||||
}
|
||||
|
||||
@objc func close() {
|
||||
dismiss(animated: true)
|
||||
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() {
|
||||
guard statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) != initialText else {
|
||||
close()
|
||||
return
|
||||
}
|
||||
|
||||
if Preferences.shared.automaticallySaveDrafts {
|
||||
saveDraft()
|
||||
close()
|
||||
return
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
alert.addAction(UIAlertAction(title: "Save draft", style: .default, handler: { (_) in
|
||||
self.saveDraft()
|
||||
self.close()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Delete draft", style: .destructive, handler: { (_) in
|
||||
if let currentDraft = self.currentDraft {
|
||||
DraftsManager.shared.remove(currentDraft)
|
||||
}
|
||||
self.close()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc func contentWarningButtonPressed() {
|
||||
contentWarningEnabled = !contentWarningEnabled
|
||||
if contentWarningEnabled {
|
||||
contentWarningTextField.becomeFirstResponder()
|
||||
} else {
|
||||
statusTextView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func contentWarningTextFieldDidChange() {
|
||||
updateCharactersRemaining()
|
||||
updateHasChanges()
|
||||
}
|
||||
|
||||
@objc func visibilityButtonPressed() {
|
||||
let alertController = UIAlertController(currentVisibility: self.visibility) { (visibility) in
|
||||
guard let visibility = visibility else { return }
|
||||
self.visibility = visibility
|
||||
}
|
||||
alertController.popoverPresentationController?.barButtonItem = visibilityBarButtonItem
|
||||
present(alertController, animated: true)
|
||||
}
|
||||
|
||||
@objc func formatButtonPressed(_ button: UIBarButtonItem) {
|
||||
guard statusTextView.isFirstResponder else {
|
||||
return
|
||||
}
|
||||
|
||||
let format = StatusFormat.allCases[button.tag]
|
||||
guard let insertionResult = format.insertionResult else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentSelectedRange = statusTextView.selectedRange
|
||||
if currentSelectedRange.length == 0 {
|
||||
statusTextView.insertText(insertionResult.prefix + insertionResult.suffix)
|
||||
statusTextView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: 0)
|
||||
} else {
|
||||
let start = statusTextView.text.index(statusTextView.text.startIndex, offsetBy: currentSelectedRange.lowerBound)
|
||||
let end = statusTextView.text.index(statusTextView.text.startIndex, offsetBy: currentSelectedRange.upperBound)
|
||||
let selectedText = statusTextView.text[start..<end]
|
||||
statusTextView.insertText(String(insertionResult.prefix + selectedText + insertionResult.suffix))
|
||||
statusTextView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.prefix.count, length: currentSelectedRange.length)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func draftsButtonPressed() {
|
||||
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!)
|
||||
draftsVC.delegate = self
|
||||
present(UINavigationController(rootViewController: draftsVC), animated: true)
|
||||
}
|
||||
|
||||
@objc func postButtonPressed() {
|
||||
guard let text = statusTextView.text,
|
||||
!text.isEmpty else { return }
|
||||
|
||||
// save a draft before posting the status, so if a crash occurs during posting, the status won't be lost
|
||||
saveDraft()
|
||||
|
||||
// disable post button while sending post request
|
||||
compositionState.formUnion(.currentlyPosting)
|
||||
|
||||
let contentWarning: String?
|
||||
if contentWarningEnabled, let cwText = contentWarningTextField.text, !cwText.isEmpty {
|
||||
contentWarning = cwText
|
||||
} else {
|
||||
contentWarning = nil
|
||||
}
|
||||
let sensitive = contentWarning != nil
|
||||
let visibility = self.visibility!
|
||||
|
||||
postProgressView.steps = 2 + (composeAttachmentsViewController.attachments.count * 2) // 2 steps (request data, then upload) for each attachment
|
||||
postProgressView.currentStep = 1
|
||||
|
||||
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: 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
|
||||
// self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: true)
|
||||
|
||||
if let draft = self.currentDraft {
|
||||
DraftsManager.shared.remove(draft)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
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)
|
||||
|
||||
self.xcbSession?.complete(with: .success, additionalData: [
|
||||
"statusURL": status.url?.absoluteString,
|
||||
"statusURI": status.uri
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeViewController: UIScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard let replyView = replyView else { return }
|
||||
|
||||
var constant: CGFloat = 8
|
||||
|
||||
if scrollView.contentOffset.y < 0 {
|
||||
constant -= scrollView.contentOffset.y
|
||||
replyAvatarImageViewTopConstraint?.constant = 8 - scrollView.contentOffset.y
|
||||
} else if scrollView.contentOffset.y > replyView.frame.height - replyView.avatarImageView.frame.height - 16 {
|
||||
constant += replyView.frame.height - replyView.avatarImageView.frame.height - 16 - scrollView.contentOffset.y
|
||||
}
|
||||
|
||||
replyAvatarImageViewTopConstraint?.constant = constant
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewController: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
updateCharactersRemaining()
|
||||
updatePlaceholder()
|
||||
updateHasChanges()
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewController: ComposeAttachmentsViewControllerDelegate {
|
||||
func composeSelectedAttachmentsDidChange() {
|
||||
currentDraft?.attachments = composeAttachmentsViewController.attachments
|
||||
}
|
||||
|
||||
func composeRequiresAttachmentDescriptionsDidChange() {
|
||||
if composeAttachmentsViewController.requiresAttachmentDescriptions {
|
||||
compositionState.formUnion(.requiresAttachmentDescriptions)
|
||||
} else {
|
||||
compositionState.subtract(.requiresAttachmentDescriptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewController: DraftsTableViewControllerDelegate {
|
||||
func draftSelectionCanceled() {
|
||||
}
|
||||
|
||||
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) {
|
||||
if draft.inReplyToID != self.inReplyToID, hasChanges {
|
||||
let alertController = UIAlertController(title: "Different Reply", message: "The selected draft is a reply to a different status, do you wish to use it?", preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
|
||||
completion(false)
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: "Restore Draft", style: .default, handler: { (_) in
|
||||
completion(true)
|
||||
}))
|
||||
// we can't present the alert ourselves, since the compose VC is already presenting the draft selector
|
||||
// but presenting on the presented view controller seems hacky, is there a better way to do this?
|
||||
presentedViewController!.present(alertController, animated: true)
|
||||
} else {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
func draftSelected(_ draft: DraftsManager.Draft) {
|
||||
if hasChanges {
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
self.currentDraft = draft
|
||||
|
||||
inReplyToID = draft.inReplyToID
|
||||
updateInReplyTo()
|
||||
|
||||
statusTextView.text = draft.text
|
||||
contentWarningEnabled = draft.contentWarning != nil
|
||||
contentWarningTextField.text = draft.contentWarning
|
||||
|
||||
updatePlaceholder()
|
||||
updateCharactersRemaining()
|
||||
|
||||
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, 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 - composeAttachmentsViewController.attachments.count
|
||||
// todo: localize me
|
||||
let suffix = difference == 1 ? "" : "s"
|
||||
let verb = difference == 1 ? "was" : "were"
|
||||
let alertController = UIAlertController(title: "Missing Attachments", message: "\(difference) attachment\(suffix) \(verb) removed from the Photos Library and could not be loaded.", preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
||||
|
||||
present(alertController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||
return Preferences.shared.automaticallySaveDrafts || !hasChanges
|
||||
}
|
||||
|
||||
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
|
||||
showSaveAndClosePrompt()
|
||||
}
|
||||
|
||||
// when the compose screen is dismissed interactively, close() isn't called, so we make sure to
|
||||
// complete the X-Callback-URL session and save the draft is automatic saving is enabled
|
||||
// (if automatic saving is off, the draft will get saved/discarded by the user when didAttemptToDismiss is called
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
if Preferences.shared.automaticallySaveDrafts {
|
||||
saveDraft()
|
||||
}
|
||||
xcbSession?.complete(with: .cancel)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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="16087"/>
|
||||
<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="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"/>
|
||||
<outlet property="inReplyToContainer" destination="2Dv-Q7-UEA" id="hfG-5j-G5R"/>
|
||||
<outlet property="inReplyToLabel" destination="Y25-eP-tDE" id="9Ei-3s-dAx"/>
|
||||
<outlet property="placeholderLabel" destination="EW3-YK-vPC" id="Rsw-Nv-TNz"/>
|
||||
<outlet property="postProgressView" destination="Tq7-6P-hMT" id="amT-F1-JI0"/>
|
||||
<outlet property="scrollView" destination="6Z0-Vy-hMX" id="ya0-2T-QaV"/>
|
||||
<outlet property="selfDetailView" destination="zZ3-Gv-4P5" id="jou-Vl-TQE"/>
|
||||
<outlet property="stackView" destination="bOB-hF-O9w" id="lD7-b2-MWl"/>
|
||||
<outlet property="statusTextView" destination="9pn-0T-IHb" id="u7j-KW-zCo"/>
|
||||
<outlet property="view" destination="7XG-Dk-OGm" id="09I-sr-hnP"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="7XG-Dk-OGm">
|
||||
<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" 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" 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" 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"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" placeholderIntrinsicWidth="infinite" placeholderIntrinsicHeight="66" translatesAutoresizingMaskIntoConstraints="NO" id="zZ3-Gv-4P5" customClass="LargeAccountDetailView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="336" height="66"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" notEnabled="YES"/>
|
||||
</accessibility>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="500" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PMB-Wa-Ht0">
|
||||
<rect key="frame" x="336" y="8" width="31" height="20.5"/>
|
||||
<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>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="PMB-Wa-Ht0" secondAttribute="trailing" constant="8" id="5KJ-rz-Heh"/>
|
||||
<constraint firstItem="zZ3-Gv-4P5" firstAttribute="leading" secondItem="6V0-mH-Mhu" secondAttribute="leading" id="f6Q-fK-zq1"/>
|
||||
<constraint firstItem="zZ3-Gv-4P5" firstAttribute="top" secondItem="6V0-mH-Mhu" secondAttribute="top" id="fjf-mn-l9f"/>
|
||||
<constraint firstItem="PMB-Wa-Ht0" firstAttribute="top" secondItem="6V0-mH-Mhu" secondAttribute="top" constant="8" id="q3V-aY-t9K"/>
|
||||
<constraint firstAttribute="bottom" secondItem="zZ3-Gv-4P5" secondAttribute="bottom" id="rOO-0n-odM"/>
|
||||
<constraint firstItem="PMB-Wa-Ht0" firstAttribute="leading" secondItem="zZ3-Gv-4P5" secondAttribute="trailing" id="sVv-tH-7eB"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2Dv-Q7-UEA">
|
||||
<rect key="frame" x="0.0" y="66" width="375" height="33.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="In reply to Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Y25-eP-tDE">
|
||||
<rect key="frame" x="4" y="8" width="367" height="21.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="21.5" id="man-Xn-eVt"/>
|
||||
</constraints>
|
||||
<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>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="Y25-eP-tDE" secondAttribute="bottom" constant="4" id="1sZ-CX-GDU"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Y25-eP-tDE" secondAttribute="trailing" constant="4" id="I31-Rs-QwW"/>
|
||||
<constraint firstItem="Y25-eP-tDE" firstAttribute="leading" secondItem="2Dv-Q7-UEA" secondAttribute="leading" constant="4" id="kdQ-zs-u7N"/>
|
||||
<constraint firstItem="Y25-eP-tDE" firstAttribute="top" secondItem="2Dv-Q7-UEA" secondAttribute="top" constant="8" id="qdC-S5-CgV"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kU2-7l-MSy">
|
||||
<rect key="frame" x="0.0" y="99.5" width="375" height="42"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Write your warning here" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="T05-p6-vTz">
|
||||
<rect key="frame" x="4" y="4" width="367" height="30"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="yzY-MF-Ukx"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<textInputTraits key="textInputTraits"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="T05-p6-vTz" secondAttribute="trailing" constant="4" id="8tG-eW-TG4"/>
|
||||
<constraint firstAttribute="bottom" secondItem="T05-p6-vTz" secondAttribute="bottom" constant="8" id="SUL-Hk-uvM"/>
|
||||
<constraint firstItem="T05-p6-vTz" firstAttribute="leading" secondItem="kU2-7l-MSy" secondAttribute="leading" constant="4" id="WGG-B2-lPC"/>
|
||||
<constraint firstItem="T05-p6-vTz" firstAttribute="top" secondItem="kU2-7l-MSy" secondAttribute="top" constant="4" id="dN2-Pf-qFQ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lhQ-ae-pe9">
|
||||
<rect key="frame" x="0.0" y="141.5" width="375" height="150"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="9pn-0T-IHb">
|
||||
<rect key="frame" x="4" y="0.0" width="367" height="150"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="150" id="ISI-jm-FxV"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="What's on your mind?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EW3-YK-vPC">
|
||||
<rect key="frame" x="8" y="8" width="188" height="24"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="9pn-0T-IHb" secondAttribute="bottom" id="UAs-fL-Riv"/>
|
||||
<constraint firstItem="9pn-0T-IHb" firstAttribute="leading" secondItem="lhQ-ae-pe9" secondAttribute="leading" constant="4" id="ezI-15-Yd4"/>
|
||||
<constraint firstItem="9pn-0T-IHb" firstAttribute="top" secondItem="lhQ-ae-pe9" secondAttribute="top" id="n8v-pK-I9E"/>
|
||||
<constraint firstItem="EW3-YK-vPC" firstAttribute="leading" secondItem="9pn-0T-IHb" secondAttribute="leading" constant="4" id="n9v-mJ-gz3"/>
|
||||
<constraint firstItem="EW3-YK-vPC" firstAttribute="top" secondItem="9pn-0T-IHb" secondAttribute="top" constant="8" id="q5e-yM-bS4"/>
|
||||
<constraint firstAttribute="trailing" secondItem="9pn-0T-IHb" secondAttribute="trailing" constant="4" id="x7Z-8w-xgm"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<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>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="bOB-hF-O9w" secondAttribute="trailing" id="GAR-qc-jte"/>
|
||||
<constraint firstAttribute="height" secondItem="bOB-hF-O9w" secondAttribute="height" id="KO2-zF-s7P"/>
|
||||
<constraint firstItem="bOB-hF-O9w" firstAttribute="top" secondItem="pcX-rB-RxJ" secondAttribute="top" id="aBm-Ub-TpI"/>
|
||||
<constraint firstItem="bOB-hF-O9w" firstAttribute="leading" secondItem="pcX-rB-RxJ" secondAttribute="leading" id="yOt-hH-L57"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="pcX-rB-RxJ" firstAttribute="leading" secondItem="6Z0-Vy-hMX" secondAttribute="leading" id="X0f-ja-XBF"/>
|
||||
<constraint firstAttribute="bottom" secondItem="pcX-rB-RxJ" secondAttribute="bottom" id="X8a-PA-r8F"/>
|
||||
<constraint firstAttribute="trailing" secondItem="pcX-rB-RxJ" secondAttribute="trailing" id="lh7-xn-MGp"/>
|
||||
<constraint firstItem="pcX-rB-RxJ" firstAttribute="top" secondItem="6Z0-Vy-hMX" secondAttribute="top" id="yMM-IS-8K1"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Tq7-6P-hMT" customClass="SteppedProgressView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="2"/>
|
||||
<color key="trackTintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</progressView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="Tq7-6P-hMT" secondAttribute="trailing" id="GeN-8q-weq"/>
|
||||
<constraint firstAttribute="bottom" secondItem="6Z0-Vy-hMX" secondAttribute="bottom" id="Hf3-Cc-mVX"/>
|
||||
<constraint firstItem="Tq7-6P-hMT" firstAttribute="top" secondItem="Heg-g4-sYM" secondAttribute="top" id="LgA-xu-VGE"/>
|
||||
<constraint firstItem="Tq7-6P-hMT" firstAttribute="leading" secondItem="7XG-Dk-OGm" secondAttribute="leading" id="agM-ZO-c3E"/>
|
||||
<constraint firstItem="Heg-g4-sYM" firstAttribute="trailing" secondItem="6Z0-Vy-hMX" secondAttribute="trailing" id="hjY-W6-wTQ"/>
|
||||
<constraint firstItem="bOB-hF-O9w" firstAttribute="width" secondItem="7XG-Dk-OGm" secondAttribute="width" id="i0p-NE-ca1"/>
|
||||
<constraint firstItem="6Z0-Vy-hMX" firstAttribute="leading" secondItem="Heg-g4-sYM" secondAttribute="leading" id="lFF-yC-ql9"/>
|
||||
<constraint firstItem="6Z0-Vy-hMX" firstAttribute="top" secondItem="Heg-g4-sYM" secondAttribute="top" id="osv-zq-seP"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Heg-g4-sYM"/>
|
||||
<point key="canvasLocation" x="140" y="154"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
|
@ -0,0 +1,193 @@
|
|||
//
|
||||
// MainComposeTextView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/29/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Pachyderm
|
||||
|
||||
struct MainComposeTextView: View {
|
||||
@ObservedObject var draft: Draft
|
||||
let placeholder: Text
|
||||
|
||||
let minHeight: CGFloat = 150
|
||||
@State private var height: CGFloat?
|
||||
@State private var becomeFirstResponder: Bool = false
|
||||
@State private var hasFirstAppeared = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
MainComposeWrappedTextView(
|
||||
text: $draft.text,
|
||||
visibility: draft.visibility,
|
||||
becomeFirstResponder: $becomeFirstResponder
|
||||
) { (textView) in
|
||||
self.height = max(textView.contentSize.height, minHeight)
|
||||
}
|
||||
.frame(height: height ?? minHeight)
|
||||
|
||||
if draft.text.isEmpty {
|
||||
placeholder
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.secondary)
|
||||
.offset(x: 4, y: 8)
|
||||
}
|
||||
}.onAppear {
|
||||
if !hasFirstAppeared {
|
||||
hasFirstAppeared = true
|
||||
becomeFirstResponder = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||
typealias UIViewType = UITextView
|
||||
|
||||
@Binding var text: String
|
||||
let visibility: Status.Visibility
|
||||
@Binding var becomeFirstResponder: Bool
|
||||
var textDidChange: (UITextView) -> Void
|
||||
|
||||
@EnvironmentObject var uiState: ComposeUIState
|
||||
@State var visibilityButton: UIBarButtonItem?
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let textView = UITextView()
|
||||
textView.delegate = context.coordinator
|
||||
textView.isEditable = true
|
||||
textView.backgroundColor = .secondarySystemBackground
|
||||
textView.font = .systemFont(ofSize: 20)
|
||||
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||
context.coordinator.textView = textView
|
||||
|
||||
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: #selector(ComposeHostingController.visibilityButtonPressed(_:)))
|
||||
let toolbar = UIToolbar()
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
toolbar.items = [
|
||||
UIBarButtonItem(title: "CW", style: .plain, target: nil, action: #selector(ComposeHostingController.cwButtonPressed)),
|
||||
visibilityButton,
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
] + createFormattingButtons(coordinator: context.coordinator) + [
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
UIBarButtonItem(title: "Drafts", style: .plain, target: nil, action: #selector(ComposeHostingController.draftsButtonPresed)),
|
||||
]
|
||||
textView.inputAccessoryView = toolbar
|
||||
// can't modify @State during view update
|
||||
DispatchQueue.main.async {
|
||||
self.visibilityButton = visibilityButton
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
private func createFormattingButtons(coordinator: Coordinator) -> [UIBarButtonItem] {
|
||||
guard Preferences.shared.statusContentType != .plain else {
|
||||
return []
|
||||
}
|
||||
|
||||
var formatButtons = StatusFormat.allCases.map { (format) -> UIBarButtonItem in
|
||||
let item: UIBarButtonItem
|
||||
if let image = format.image {
|
||||
item = UIBarButtonItem(image: image, style: .plain, target: coordinator, action: #selector(Coordinator.formatButtonPressed(_:)))
|
||||
} else if let (str, attributes) = format.title {
|
||||
item = UIBarButtonItem(title: str, style: .plain, target: coordinator, action: #selector(Coordinator.formatButtonPressed(_:)))
|
||||
item.setTitleTextAttributes(attributes, for: .normal)
|
||||
item.setTitleTextAttributes(attributes, for: .highlighted)
|
||||
} else {
|
||||
fatalError("StatusFormat must have either an image or a title")
|
||||
}
|
||||
item.tag = StatusFormat.allCases.firstIndex(of: format)!
|
||||
item.accessibilityLabel = format.accessibilityLabel
|
||||
return item
|
||||
}
|
||||
|
||||
for i in (1..<StatusFormat.allCases.count).reversed() {
|
||||
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
|
||||
spacer.width = 8
|
||||
formatButtons.insert(spacer, at: i)
|
||||
}
|
||||
|
||||
return formatButtons
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||
uiView.text = text
|
||||
visibilityButton?.image = UIImage(systemName: visibility.imageName)
|
||||
context.coordinator.text = $text
|
||||
context.coordinator.didChange = textDidChange
|
||||
context.coordinator.uiState = uiState
|
||||
|
||||
if becomeFirstResponder {
|
||||
uiView.becomeFirstResponder()
|
||||
DispatchQueue.main.async {
|
||||
becomeFirstResponder = false
|
||||
}
|
||||
}
|
||||
|
||||
// wait until the next runloop iteration so that SwiftUI view updates have finished and
|
||||
// the text view knows its new content size
|
||||
DispatchQueue.main.async {
|
||||
self.textDidChange(uiView)
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator(text: $text, uiState: uiState, didChange: textDidChange)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextViewDelegate {
|
||||
weak var textView: UITextView?
|
||||
var text: Binding<String>
|
||||
var didChange: (UITextView) -> Void
|
||||
var uiState: ComposeUIState
|
||||
|
||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||
self.text = text
|
||||
self.didChange = didChange
|
||||
self.uiState = uiState
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
text.wrappedValue = textView.text
|
||||
didChange(textView)
|
||||
}
|
||||
|
||||
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
||||
guard let textView = textView, textView.isFirstResponder else { return }
|
||||
let format = StatusFormat.allCases[sender.tag]
|
||||
guard let insertionResult = format.insertionResult else { return }
|
||||
|
||||
let currentSelectedRange = textView.selectedRange
|
||||
if currentSelectedRange.length == 0 {
|
||||
textView.insertText(insertionResult.prefix + insertionResult.suffix)
|
||||
textView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: 0)
|
||||
} else {
|
||||
let start = textView.text.index(textView.text.startIndex, offsetBy: currentSelectedRange.lowerBound)
|
||||
let end = textView.text.index(textView.text.startIndex, offsetBy: currentSelectedRange.upperBound)
|
||||
let selectedText = textView.text[start..<end]
|
||||
textView.insertText(String(insertionResult.prefix + selectedText + insertionResult.suffix))
|
||||
textView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.prefix.utf16.count, length: currentSelectedRange.length)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func keyboardWillShow(_ notification: Foundation.Notification) {
|
||||
uiState.delegate?.keyboardWillShow(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
||||
}
|
||||
|
||||
@objc func keyboardWillHide(_ notification: Foundation.Notification) {
|
||||
uiState.delegate?.keyboardWillHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
||||
}
|
||||
|
||||
@objc func keyboardDidHide(_ notification: Foundation.Notification) {
|
||||
uiState.delegate?.keyboardDidHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,20 +10,22 @@ import UIKit
|
|||
|
||||
protocol DraftsTableViewControllerDelegate: class {
|
||||
func draftSelectionCanceled()
|
||||
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void)
|
||||
func draftSelected(_ draft: DraftsManager.Draft)
|
||||
func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void)
|
||||
func draftSelected(_ draft: Draft)
|
||||
func draftSelectionCompleted()
|
||||
}
|
||||
|
||||
class DraftsTableViewController: UITableViewController {
|
||||
|
||||
let account: LocalData.UserAccountInfo
|
||||
let excludedDraft: Draft?
|
||||
weak var delegate: DraftsTableViewControllerDelegate?
|
||||
|
||||
var drafts = [DraftsManager.Draft]()
|
||||
var drafts = [Draft]()
|
||||
|
||||
init(account: LocalData.UserAccountInfo) {
|
||||
init(account: LocalData.UserAccountInfo, exclude: Draft? = nil) {
|
||||
self.account = account
|
||||
self.excludedDraft = exclude
|
||||
|
||||
super.init(nibName: "DraftsTableViewController", bundle: nil)
|
||||
|
||||
|
@ -44,11 +46,11 @@ class DraftsTableViewController: UITableViewController {
|
|||
tableView.register(UINib(nibName: "DraftTableViewCell", bundle: nil), forCellReuseIdentifier: "draftCell")
|
||||
|
||||
drafts = DraftsManager.shared.sorted.filter { (draft) in
|
||||
draft.accountID == account.id
|
||||
draft.accountID == account.id && draft != excludedDraft
|
||||
}
|
||||
}
|
||||
|
||||
func draft(for indexPath: IndexPath) -> DraftsManager.Draft {
|
||||
func draft(for indexPath: IndexPath) -> Draft {
|
||||
return drafts[indexPath.row]
|
||||
}
|
||||
|
||||
|
|
|
@ -309,10 +309,10 @@ fileprivate extension MainSidebarViewController.Item {
|
|||
@available(iOS 14.0, *)
|
||||
extension MainSplitViewController: TuskerRootViewController {
|
||||
func presentCompose() {
|
||||
let compose = ComposeViewController(mastodonController: mastodonController)
|
||||
let navigationController = EnhancedNavigationViewController(rootViewController: compose)
|
||||
navigationController.presentationController?.delegate = compose
|
||||
present(navigationController, animated: true)
|
||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||
nav.presentationController?.delegate = vc
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
func select(tab: MainTabBarViewController.Tab) {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||
|
||||
|
@ -101,7 +102,7 @@ extension MainTabBarViewController {
|
|||
case .notifications:
|
||||
return NotificationsPageViewController(mastodonController: mastodonController)
|
||||
case .compose:
|
||||
return ComposeViewController(mastodonController: mastodonController)
|
||||
return ComposeHostingController(mastodonController: mastodonController)
|
||||
case .explore:
|
||||
return ExploreViewController(mastodonController: mastodonController)
|
||||
case .myProfile:
|
||||
|
@ -121,10 +122,10 @@ extension MainTabBarViewController {
|
|||
|
||||
extension MainTabBarViewController: TuskerRootViewController {
|
||||
func presentCompose() {
|
||||
let compose = ComposeViewController(mastodonController: mastodonController)
|
||||
let navigationController = embedInNavigationController(compose)
|
||||
navigationController.presentationController?.delegate = compose
|
||||
present(navigationController, animated: true)
|
||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||
nav.presentationController?.delegate = vc
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
func select(tab: Tab) {
|
||||
|
|
|
@ -40,7 +40,7 @@ extension MenuPreviewProvider {
|
|||
var actionsSection: [UIMenuElement] = [
|
||||
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.compose(mentioning: account.acct)
|
||||
self.navigationDelegate?.compose(mentioningAcct: account.acct)
|
||||
}),
|
||||
]
|
||||
|
||||
|
@ -129,7 +129,7 @@ extension MenuPreviewProvider {
|
|||
var actionsSection = [
|
||||
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.reply(to: statusID)
|
||||
self.navigationDelegate?.compose(inReplyToID: statusID)
|
||||
}),
|
||||
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Intents
|
||||
import Pachyderm
|
||||
|
||||
class UserActivityManager {
|
||||
|
@ -50,7 +51,8 @@ class UserActivityManager {
|
|||
static func handleNewPost(activity: NSUserActivity) {
|
||||
// TODO: check not currently showing compose screen
|
||||
let mentioning = activity.userInfo?["mentioning"] as? String
|
||||
let composeVC = ComposeViewController(mentioningAcct: mentioning, mastodonController: mastodonController)
|
||||
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
||||
let composeVC = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
||||
present(UINavigationController(rootViewController: composeVC))
|
||||
}
|
||||
|
||||
|
|
|
@ -79,24 +79,10 @@ extension TuskerNavigationDelegate {
|
|||
show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
||||
}
|
||||
|
||||
// protocols can't have parameter defaults, so this stub is necessary to fulfill the protocol req
|
||||
func compose() {
|
||||
compose(mentioning: nil)
|
||||
}
|
||||
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
||||
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||
|
||||
func compose(mentioning: String?) {
|
||||
let compose = ComposeViewController(mentioningAcct: mentioning, mastodonController: apiController)
|
||||
let vc = UINavigationController(rootViewController: compose)
|
||||
vc.presentationController?.delegate = compose
|
||||
present(vc, animated: true)
|
||||
}
|
||||
|
||||
func reply(to statusID: String) {
|
||||
reply(to: statusID, mentioningAcct: nil)
|
||||
}
|
||||
|
||||
func reply(to statusID: String, mentioningAcct: String?) {
|
||||
let compose = ComposeViewController(inReplyTo: statusID, mentioningAcct: mentioningAcct, mastodonController: apiController)
|
||||
let vc = UINavigationController(rootViewController: compose)
|
||||
vc.presentationController?.delegate = compose
|
||||
present(vc, animated: true)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// ActivityIndicatorView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/29/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActivityIndicatorView: UIViewRepresentable {
|
||||
typealias UIViewType = UIActivityIndicatorView
|
||||
|
||||
func makeUIView(context: Context) -> UIActivityIndicatorView {
|
||||
let view = UIActivityIndicatorView()
|
||||
view.startAnimating()
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
<?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>
|
|
@ -1,176 +0,0 @@
|
|||
//
|
||||
// ComposeAttachmentTableViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 3/13/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
import AVFoundation
|
||||
import Vision
|
||||
|
||||
protocol ComposeAttachmentTableViewCellDelegate: class {
|
||||
func composeAttachment(_ cell: ComposeAttachmentTableViewCell, present viewController: UIViewController, animated: Bool)
|
||||
func removeAttachment(_ cell: ComposeAttachmentTableViewCell)
|
||||
func attachmentDescriptionChanged(_ cell: ComposeAttachmentTableViewCell)
|
||||
func composeAttachmentDescriptionHeightChanged(_ cell: ComposeAttachmentTableViewCell)
|
||||
}
|
||||
|
||||
class ComposeAttachmentTableViewCell: UITableViewCell {
|
||||
|
||||
weak var delegate: ComposeAttachmentTableViewCellDelegate?
|
||||
|
||||
@IBOutlet weak var assetImageView: UIImageView!
|
||||
@IBOutlet weak var descriptionTextView: UITextView!
|
||||
@IBOutlet weak var descriptionTextViewHeightConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var descriptionPlaceholderLabel: UILabel!
|
||||
@IBOutlet weak var removeButton: UIButton!
|
||||
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
|
||||
|
||||
var attachment: CompositionAttachment!
|
||||
|
||||
var state: State = .allowEntry {
|
||||
didSet {
|
||||
switch state {
|
||||
case .allowEntry:
|
||||
descriptionTextView.isEditable = true
|
||||
updateDescriptionPlaceholderLabel()
|
||||
activityIndicator.stopAnimating()
|
||||
case .recognizingText:
|
||||
descriptionTextView.isEditable = false
|
||||
descriptionPlaceholderLabel.isHidden = true
|
||||
activityIndicator.startAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var textRecognitionRequest: VNRecognizeTextRequest?
|
||||
|
||||
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()
|
||||
|
||||
assetImageView.contentMode = .scaleAspectFill
|
||||
assetImageView.backgroundColor = .secondarySystemBackground
|
||||
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)
|
||||
}
|
||||
case let .drawing(drawing):
|
||||
assetImageView.image = drawing.imageInLightMode(from: drawing.bounds)
|
||||
assetImageView.contentMode = .scaleAspectFit
|
||||
assetImageView.backgroundColor = .white
|
||||
}
|
||||
}
|
||||
|
||||
func updateDescriptionPlaceholderLabel() {
|
||||
descriptionPlaceholderLabel.isHidden = !descriptionTextView.text.isEmpty
|
||||
}
|
||||
|
||||
func setEnabled(_ enabled: Bool) {
|
||||
descriptionTextView.isEditable = enabled
|
||||
removeButton.isEnabled = enabled
|
||||
}
|
||||
|
||||
func recognizeTextFromImage() {
|
||||
precondition(attachment.data.type == .image)
|
||||
state = .recognizingText
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.attachment.data.getData { (data, mimeType) in
|
||||
let handler = VNImageRequestHandler(data: data, options: [:])
|
||||
let request = VNRecognizeTextRequest { (request, error) in
|
||||
DispatchQueue.main.async {
|
||||
self.state = .allowEntry
|
||||
|
||||
if let results = request.results as? [VNRecognizedTextObservation] {
|
||||
var text = ""
|
||||
for observation in results {
|
||||
let result = observation.topCandidates(1).first!
|
||||
text.append(result.string)
|
||||
text.append("\n")
|
||||
}
|
||||
self.descriptionTextView.text = text
|
||||
self.textViewDidChange(self.descriptionTextView)
|
||||
}
|
||||
}
|
||||
}
|
||||
request.recognitionLevel = .accurate
|
||||
request.usesLanguageCorrection = true
|
||||
self.textRecognitionRequest = request
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch {
|
||||
// The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for.
|
||||
guard (error as NSError).code != 1 else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.state = .allowEntry
|
||||
let title = NSLocalizedString("Text Recognition Failed", comment: "text recognition error alert title")
|
||||
let message = error.localizedDescription
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
self.delegate?.composeAttachment(self, present: alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
assetImageView.image = nil
|
||||
descriptionTextViewHeightConstraint.constant = 80
|
||||
}
|
||||
|
||||
@IBAction func removeButtonPressed(_ sender: Any) {
|
||||
textRecognitionRequest?.cancel()
|
||||
delegate?.removeAttachment(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeAttachmentTableViewCell {
|
||||
enum State {
|
||||
case allowEntry, recognizingText
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeAttachmentTableViewCell: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
attachment.attachmentDescription = textView.text
|
||||
updateDescriptionPlaceholderLabel()
|
||||
delegate?.attachmentDescriptionChanged(self)
|
||||
let smallestSize = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: .greatestFiniteMagnitude))
|
||||
let old = descriptionTextViewHeightConstraint.constant
|
||||
descriptionTextViewHeightConstraint.constant = max(80, smallestSize.height)
|
||||
if old != descriptionTextViewHeightConstraint.constant {
|
||||
delegate?.composeAttachmentDescriptionHeightChanged(self)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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="16087"/>
|
||||
<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"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="80" id="6aZ-w8-j9n"/>
|
||||
</constraints>
|
||||
<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" pointerInteraction="YES" 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>
|
||||
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="Kzy-5r-UW8">
|
||||
<rect key="frame" x="179" y="38" width="20" height="20"/>
|
||||
</activityIndicatorView>
|
||||
</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="Kzy-5r-UW8" firstAttribute="centerX" secondItem="cwP-Eh-5dJ" secondAttribute="centerX" id="czP-Ia-Ddc"/>
|
||||
<constraint firstItem="Kzy-5r-UW8" firstAttribute="centerY" secondItem="cwP-Eh-5dJ" secondAttribute="centerY" id="eel-xx-aFq"/>
|
||||
<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="activityIndicator" destination="Kzy-5r-UW8" id="lmy-NY-Owu"/>
|
||||
<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="descriptionTextViewHeightConstraint" destination="6aZ-w8-j9n" id="ees-sT-Trc"/>
|
||||
<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>
|
|
@ -17,9 +17,9 @@ class DraftTableViewCell: UITableViewCell {
|
|||
@IBOutlet weak var attachmentsStackViewContainer: UIView!
|
||||
@IBOutlet weak var attachmentsStackView: UIStackView!
|
||||
|
||||
func updateUI(for draft: DraftsManager.Draft) {
|
||||
func updateUI(for draft: Draft) {
|
||||
contentWarningLabel.text = draft.contentWarning
|
||||
contentWarningLabel.isHidden = draft.contentWarning == nil
|
||||
contentWarningLabel.isHidden = !draft.contentWarningEnabled
|
||||
contentLabel.text = draft.text
|
||||
lastModifiedLabel.text = draft.lastModified.timeAgoString()
|
||||
|
||||
|
|
|
@ -300,7 +300,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
@IBAction func replyPressed() {
|
||||
delegate?.reply(to: statusID)
|
||||
delegate?.compose(inReplyToID: statusID)
|
||||
}
|
||||
|
||||
@IBAction func favoritePressed() {
|
||||
|
|
|
@ -153,9 +153,9 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
if Preferences.shared.mentionReblogger,
|
||||
let rebloggerID = rebloggerID,
|
||||
let rebloggerAccount = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||
delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct)
|
||||
delegate?.compose(inReplyToID: statusID, mentioningAcct: rebloggerAccount.acct)
|
||||
} else {
|
||||
delegate?.reply(to: statusID)
|
||||
delegate?.compose(inReplyToID: statusID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// WrappedProgressView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/30/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WrappedProgressView: UIViewRepresentable {
|
||||
typealias UIViewType = UIProgressView
|
||||
|
||||
let value: Double
|
||||
let total: Double
|
||||
|
||||
func makeUIView(context: Context) -> UIProgressView {
|
||||
return UIProgressView(progressViewStyle: .bar)
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIProgressView, context: Context) {
|
||||
if total > 0 {
|
||||
uiView.setProgress(Float(value / total), animated: true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -158,8 +158,10 @@ struct XCBActions {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
let compose = ComposeViewController(mentioningAcct: mentioning, text: text, mastodonController: mastodonController)
|
||||
compose.xcbSession = session
|
||||
// todo: use text param
|
||||
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
||||
// compose.xcbSession = session
|
||||
let vc = UINavigationController(rootViewController: compose)
|
||||
present(vc)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue