From 97ecbb1bfb3160392f199da158a7b4b30e3ecc45 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 Mar 2021 15:41:27 +0800 Subject: [PATCH] feat: add compose scene --- Localization/app.json | 6 ++ Mastodon.xcodeproj/project.pbxproj | 63 +++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++ Mastodon/Coordinator/SceneCoordinator.swift | 7 ++ .../Diffiable/Item/ComposeStatusItem.swift | 34 ++++++ .../Section/ComposeStatusSection.swift | 13 +++ Mastodon/Generated/Strings.swift | 8 ++ .../Resources/en.lproj/Localizable.strings | 2 + .../Scene/Compose/ComposeViewController.swift | 102 ++++++++++++++++++ .../Compose/ComposeViewModel+Diffable.swift | 40 +++++++ Mastodon/Scene/Compose/ComposeViewModel.swift | 44 ++++++++ ...oseRepliedToTootContentTableViewCell.swift | 31 ++++++ .../ComposeTootContentTableViewCell.swift | 40 +++++++ .../HomeTimelineViewController.swift | 3 +- README.md | 1 + 15 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 Mastodon/Diffiable/Item/ComposeStatusItem.swift create mode 100644 Mastodon/Diffiable/Section/ComposeStatusSection.swift create mode 100644 Mastodon/Scene/Compose/ComposeViewController.swift create mode 100644 Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift create mode 100644 Mastodon/Scene/Compose/ComposeViewModel.swift create mode 100644 Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToTootContentTableViewCell.swift create mode 100644 Mastodon/Scene/Compose/TableViewCell/ComposeTootContentTableViewCell.swift diff --git a/Localization/app.json b/Localization/app.json index 123655955..ab5b3f659 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -173,6 +173,12 @@ }, "public_timeline": { "title": "Public" + }, + "compose": { + "title": { + "new_toot": "New Toot", + "new_reply": "New Reply" + } } } } \ No newline at end of file diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 69f431909..d999c0e81 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -149,6 +149,11 @@ DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; }; DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; }; DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* StripProgressView.swift */; }; + DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */; }; + DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; }; + DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; }; + DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; }; + DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; }; DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; @@ -156,6 +161,10 @@ DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; + DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; }; + DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; }; + DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */; }; + DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; }; DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -244,6 +253,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */, DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -401,6 +411,9 @@ DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = ""; }; DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = ""; }; DB59F11725EFA35B001F1DAB /* StripProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripProgressView.swift; sourceTree = ""; }; + DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+Diffable.swift"; sourceTree = ""; }; + DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = ""; }; + DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = ""; }; DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = ""; }; DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = ""; }; DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -408,6 +421,10 @@ DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; + DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; + DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; + DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTootContentTableViewCell.swift; sourceTree = ""; }; + DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentTableViewCell.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = ""; }; DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -461,6 +478,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */, DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, @@ -718,6 +736,7 @@ DB4481C525EE2ADA00BEFB67 /* PollSection.swift */, DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */, DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, + DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, ); path = Section; sourceTree = ""; @@ -765,6 +784,7 @@ DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */, DB1E347725F519300079D7DF /* PickServerItem.swift */, DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */, + DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */, ); path = Item; sourceTree = ""; @@ -996,6 +1016,26 @@ path = ServerRules; sourceTree = ""; }; + DB789A1025F9F29B0071ACA0 /* Compose */ = { + isa = PBXGroup; + children = ( + DB789A2125F9F76D0071ACA0 /* TableViewCell */, + DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */, + DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */, + DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */, + ); + path = Compose; + sourceTree = ""; + }; + DB789A2125F9F76D0071ACA0 /* TableViewCell */ = { + isa = PBXGroup; + children = ( + DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */, + DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */, + ); + path = TableViewCell; + sourceTree = ""; + }; DB89B9EF25C10FD0008580ED /* CoreDataStack */ = { isa = PBXGroup; children = ( @@ -1097,6 +1137,7 @@ DB9D6BEE25E4F5370051B173 /* Search */, DB9D6BFD25E4F57B0051B173 /* Notification */, DB9D6C0825E4F5A60051B173 /* Profile */, + DB789A1025F9F29B0071ACA0 /* Compose */, ); path = Scene; sourceTree = ""; @@ -1253,6 +1294,7 @@ DB5086B725CC0D6400C2C187 /* Kingfisher */, 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, 2D939AC725EE14620076FA61 /* CropViewController */, + DB6672A225F9FDE500D60309 /* TwitterTextEditor */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -1382,6 +1424,7 @@ DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, + DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -1576,6 +1619,7 @@ 5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, + DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, @@ -1593,6 +1637,7 @@ 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */, 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, + DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */, 0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */, DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */, DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, @@ -1617,6 +1662,7 @@ 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */, DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */, + DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */, DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */, 2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */, DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */, @@ -1641,6 +1687,7 @@ DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, + DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, @@ -1705,10 +1752,13 @@ 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */, + DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */, 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, + DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */, 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, 5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */, DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */, + DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2295,6 +2345,14 @@ minimumVersion = 6.1.0; }; }; + DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/twitter/TwitterTextEditor.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2337,6 +2395,11 @@ package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; + DB6672A225F9FDE500D60309 /* TwitterTextEditor */ = { + isa = XCSwiftPackageProductDependency; + package = DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; + productName = TwitterTextEditor; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3183f10d5..21afdd4cd 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -99,6 +99,15 @@ "revision": "dad97167bf1be16aeecd109130900995dd01c515", "version": "2.6.0" } + }, + { + "package": "TwitterTextEditor", + "repositoryURL": "https://github.com/twitter/TwitterTextEditor.git", + "state": { + "branch": null, + "revision": "8aa914134c5b6aa46e862de63f239ec0e3b52a91", + "version": "1.0.0" + } } ] }, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index fa2963386..24d33c83f 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -47,6 +47,9 @@ extension SceneCoordinator { case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel) case mastodonResendEmail(viewModel: MastodonResendEmailViewModel) + // compose + case compose(viewModel: ComposeViewModel) + // misc case alertController(alertController: UIAlertController) @@ -190,6 +193,10 @@ private extension SceneCoordinator { let _viewController = MastodonResendEmailViewController() _viewController.viewModel = viewModel viewController = _viewController + case .compose(let viewModel): + let _viewController = ComposeViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .alertController(let alertController): if let popoverPresentationController = alertController.popoverPresentationController { assert( diff --git a/Mastodon/Diffiable/Item/ComposeStatusItem.swift b/Mastodon/Diffiable/Item/ComposeStatusItem.swift new file mode 100644 index 000000000..35977f3d4 --- /dev/null +++ b/Mastodon/Diffiable/Item/ComposeStatusItem.swift @@ -0,0 +1,34 @@ +// +// ComposeStatusItem.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-11. +// + +import Foundation +import CoreData + +enum ComposeStatusItem { + case replyTo(tootObjectID: NSManagedObjectID) + case toot(attribute: InputAttribute) +} + +extension ComposeStatusItem: Hashable { } + +extension ComposeStatusItem { + class InputAttribute: Hashable { + let hasReplyTo: Bool + + init(hasReplyTo: Bool) { + self.hasReplyTo = hasReplyTo + } + + func hash(into hasher: inout Hasher) { + hasher.combine(hasReplyTo) + } + + static func == (lhs: ComposeStatusItem.InputAttribute, rhs: ComposeStatusItem.InputAttribute) -> Bool { + return lhs.hasReplyTo == rhs.hasReplyTo + } + } +} diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift new file mode 100644 index 000000000..56b006892 --- /dev/null +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -0,0 +1,13 @@ +// +// ComposeStatusSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-11. +// + +import Foundation + +enum ComposeStatusSection: Equatable, Hashable { + case repliedTo + case status +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7c595918e..6df84fb7d 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -127,6 +127,14 @@ internal enum L10n { } internal enum Scene { + internal enum Compose { + internal enum Title { + /// New Reply + internal static let newReply = L10n.tr("Localizable", "Scene.Compose.Title.NewReply") + /// New Toot + internal static let newToot = L10n.tr("Localizable", "Scene.Compose.Title.NewToot") + } + } internal enum ConfirmEmail { /// We just sent an email to %@,\ntap the link to confirm your account. internal static func subtitle(_ p1: Any) -> String { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 54b69e274..a83819ddc 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -34,6 +34,8 @@ "Common.Controls.Timeline.LoadMore" = "Load More"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; +"Scene.Compose.Title.NewReply" = "New Reply"; +"Scene.Compose.Title.NewToot" = "New Toot"; "Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; "Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift new file mode 100644 index 000000000..f5f2746d8 --- /dev/null +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -0,0 +1,102 @@ +// +// ComposeViewController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-11. +// + +import os.log +import UIKit +import Combine +import TwitterTextEditor + +final class ComposeViewController: UIViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + var viewModel: ComposeViewModel! + + let tableView: UITableView = { + let tableView = ControlContainableTableView() + tableView.register(ComposeRepliedToTootContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self)) + tableView.register(ComposeTootContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeTootContentTableViewCell.self)) + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + return tableView + }() + +} + +extension ComposeViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = Asset.Colors.Background.systemBackground.color + viewModel.title + .receive(on: DispatchQueue.main) + .sink { [weak self] title in + guard let self = self else { return } + self.title = title + } + .store(in: &disposeBag) + navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) + + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.delegate = self + viewModel.setupDiffableDataSource(for: tableView) + + + } + +} + +extension ComposeViewController { + + @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + dismiss(animated: true, completion: nil) + } + +} + +// MARK: - TextEditorViewTextAttributesDelegate +extension ComposeViewController: TextEditorViewTextAttributesDelegate { + + func textEditorView(_ textEditorView: TextEditorView, updateAttributedString attributedString: NSAttributedString, completion: @escaping (NSAttributedString?) -> Void) { + // TODO: + } + +} + +// MARK: - UITableViewDelegate +extension ComposeViewController: UITableViewDelegate { + +} + +// MARK: - ComposeViewController +extension ComposeViewController: UIAdaptivePresentationControllerDelegate { + + func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { + return viewModel.shouldDismiss.value + } + + func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} diff --git a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift new file mode 100644 index 000000000..65a608653 --- /dev/null +++ b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift @@ -0,0 +1,40 @@ +// +// ComposeViewModel+Diffable.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-11. +// + +import UIKit + +extension ComposeViewModel { + + func setupDiffableDataSource(for tableView: UITableView) { + diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item -> UITableViewCell? in + guard let self = self else { return nil } + + switch item { + case .replyTo(let tootObjectID): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self), for: indexPath) as! ComposeRepliedToTootContentTableViewCell + // TODO: + return cell + case .toot(let attribute): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeTootContentTableViewCell.self), for: indexPath) as! ComposeTootContentTableViewCell + // TODO: + return cell + } + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.repliedTo, .status]) + switch composeKind { + case .replyToot(let tootObjectID): + snapshot.appendItems([.replyTo(tootObjectID: tootObjectID)], toSection: .repliedTo) + snapshot.appendItems([.toot(attribute: ComposeStatusItem.InputAttribute(hasReplyTo: true))], toSection: .status) + case .toot: + snapshot.appendItems([.toot(attribute: ComposeStatusItem.InputAttribute(hasReplyTo: false))], toSection: .status) + } + diffableDataSource.apply(snapshot, animatingDifferences: false) + } + +} diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift new file mode 100644 index 000000000..bcda65879 --- /dev/null +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -0,0 +1,44 @@ +// +// ComposeViewModel.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-11. +// + +import UIKit +import Combine +import CoreData +import CoreDataStack + +final class ComposeViewModel { + + // input + let context: AppContext + let composeKind: ComposeKind + + // output + var diffableDataSource: UITableViewDiffableDataSource! + let title: CurrentValueSubject + let shouldDismiss = CurrentValueSubject(true) + + init( + context: AppContext, + composeKind: ComposeKind + ) { + self.context = context + self.composeKind = composeKind + switch composeKind { + case .toot: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newToot) + case .replyToot: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply) + } + // end init + } + +} + +extension ComposeViewModel { + enum ComposeKind { + case toot + case replyToot(tootObjectID: NSManagedObjectID) + } +} diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToTootContentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToTootContentTableViewCell.swift new file mode 100644 index 000000000..def777caf --- /dev/null +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToTootContentTableViewCell.swift @@ -0,0 +1,31 @@ +// +// ComposeRepliedToTootContentTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-11. +// + +import UIKit + +final class ComposeRepliedToTootContentTableViewCell: UITableViewCell { + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ComposeRepliedToTootContentTableViewCell { + + private func _init() { + + } + +} + diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeTootContentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeTootContentTableViewCell.swift new file mode 100644 index 000000000..f26b19c62 --- /dev/null +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeTootContentTableViewCell.swift @@ -0,0 +1,40 @@ +// +// ComposeTootContentTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-11. +// + +import UIKit + +final class ComposeTootContentTableViewCell: UITableViewCell { + + let statusView = StatusView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ComposeTootContentTableViewCell { + + private func _init() { + statusView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(statusView) + NSLayoutConstraint.activate([ + statusView.topAnchor.constraint(equalTo: contentView.topAnchor), + statusView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + statusView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + statusView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + } + +} + diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index c9498dbea..19e8c3ed4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -166,7 +166,8 @@ extension HomeTimelineViewController { @objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - + let composeViewModel = ComposeViewModel(context: context, composeKind: .toot) + coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { diff --git a/README.md b/README.md index 0847c82e5..53e3bf498 100644 --- a/README.md +++ b/README.md @@ -53,5 +53,6 @@ arch -x86_64 pod install - [Kingfisher](https://github.com/onevcat/Kingfisher) - [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) +- [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor) ## License