From 0dee1bb07395c81b8f0ba05a2fd34a50e78954f5 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 12 Jun 2023 13:43:38 -0400 Subject: [PATCH] Add SiteSettingsView --- .../Blog/Settings/SiteSettingsView.swift | 126 ++++++++++++++++++ .../Blog/Settings/SiteSettingsViewModel.swift | 41 ++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 20 +++ 3 files changed, 187 insertions(+) create mode 100644 WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsView.swift create mode 100644 WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsViewModel.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsView.swift b/WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsView.swift new file mode 100644 index 000000000000..a8ede2d491bd --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsView.swift @@ -0,0 +1,126 @@ +import Foundation +import SwiftUI +import Combine +import SVProgressHUD + +struct SiteSettingsView: View { + @ObservedObject private var blog: Blog + @ObservedObject private var settings: BlogSettings + + @StateObject private var viewModel: SiteSettingsViewModel + + @SwiftUI.Environment(\.presentationMode) private var presentationMode + + init(blog: Blog) { + self.blog = blog + self.settings = blog.settings ?? BlogSettings(context: ContextManager.shared.mainContext) // Right-side should never happen + self._viewModel = StateObject(wrappedValue: SiteSettingsViewModel(blog: blog)) + } + + var body: some View { + List { + Section(header: Text(Strings.Sections.general), content: { + siteTitleRow + }) + } + .listStyle(.insetGrouped) + .navigationTitle(Strings.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if presentationMode.wrappedValue.isPresented { + closeButton + } + } + } + .onReceive(viewModel.onDismissableError) { + SVProgressHUD.showDismissibleError(withStatus: $0) + } + } + + // MARK: - General + + private var siteTitleRow: some View { + withAdminNavigationLink(destination: { + SettingsTextEditView( + value: settings.name, + placeholder: Strings.General.siteTitlePlaceholder, + onCommit: viewModel.updateSiteTitle + ) + .navigationTitle(Strings.General.siteTitle) + }) { + SettingsCell(title: Strings.General.siteTitle, value: settings.name ?? Strings.General.siteTitlePlaceholder) + } + } + + // MARK: - Helpers + + @ViewBuilder + private func withAdminNavigationLink( + @ViewBuilder destination: () -> T, + @ViewBuilder content: () -> U + ) -> some View { + if blog.isAdmin { + NavigationLink(destination: destination(), label: content) + } else { + content() + } + } + + private var closeButton: some View { + Button(action: { presentationMode.wrappedValue.dismiss() }) { + Text(Strings.done) + .font(.body.weight(.medium)) + .foregroundColor(Color.primary) + } + } +} + +private struct SettingsCell: View { + let title: String + let value: String + + var body: some View { + HStack { + Text(title) + .layoutPriority(1) + Spacer() + Text(value) + .foregroundColor(.secondary) + } + .lineLimit(1) + } +} + +private struct SettingsTextEditView: UIViewControllerRepresentable { + let value: String? + let placeholder: String + var hint: String? + let onCommit: ((String)) -> Void + + func makeUIViewController(context: Context) -> SettingsTextViewController { + let viewController = SettingsTextViewController(text: value ?? "", placeholder: placeholder, hint: hint ?? "") + viewController.onValueChanged = onCommit + return viewController + } + + func updateUIViewController(_ uiViewController: SettingsTextViewController, context: Context) { + // Do nothing + } +} + +private extension SiteSettingsView { + enum Strings { + static let title = NSLocalizedString("Settings", comment: "Title for screen that allows configuration of your blog/site settings.") + static let done = NSLocalizedString("Done", comment: "Label for Done button") + + enum Sections { + static let general = NSLocalizedString("General", comment: "Title for the general section in site settings screen") + } + + enum General { + static let siteTitle = NSLocalizedString("Site Title", comment: "Label for site title blog setting") + static let siteTitlePlaceholder = NSLocalizedString("A title for the site", comment: "Placeholder text for the title of a site") + } + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsViewModel.swift new file mode 100644 index 000000000000..a9d2a122a6bc --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsViewModel.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftUI +import Combine + +final class SiteSettingsViewModel: ObservableObject { + private let blog: Blog + private let service: BlogService + + let onDismissableError = PassthroughSubject() + + init(blog: Blog) { + self.blog = blog + self.service = BlogService(coreDataStack: ContextManager.shared) + } + + func updateSiteTitle(_ value: String) { + guard value != blog.settings?.name else { return } + blog.settings?.name = value + save() + trackSettingsChange(fieldName: "site_title") + } + + private func save() { + service.updateSettings(for: blog, success: { + NotificationCenter.default.post(name: .WPBlogSettingsUpdated, object: nil) + }, failure: { [weak self] error in + self?.onDismissableError.send(Strings.saveFailed) + DDLogError("Error while trying to update BlogSettings: \(error)") + }) + } + + private func trackSettingsChange(fieldName: String, value: Any? = nil) { + WPAnalytics.trackSettingsChange("site_settings", fieldName: fieldName, value: value) + } +} + +private extension SiteSettingsViewModel { + enum Strings { + static let saveFailed = NSLocalizedString("Settings update failed", comment: "Message to show when setting save failed") + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index ac52fc10913a..9e5a316206bc 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -346,6 +346,10 @@ 0C391E612A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E602A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift */; }; 0C391E622A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E602A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift */; }; 0C391E642A312DB20040EA91 /* DashboardBlazeCampaignViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E632A312DB20040EA91 /* DashboardBlazeCampaignViewModelTests.swift */; }; + 0C60DF792A377C3800509C91 /* SiteSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF782A377C3800509C91 /* SiteSettingsView.swift */; }; + 0C60DF7A2A377C3800509C91 /* SiteSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF782A377C3800509C91 /* SiteSettingsView.swift */; }; + 0C60DF7E2A37C90200509C91 /* SiteSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF7D2A37C90200509C91 /* SiteSettingsViewModel.swift */; }; + 0C60DF7F2A37C90200509C91 /* SiteSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF7D2A37C90200509C91 /* SiteSettingsViewModel.swift */; }; 0CB4056B29C78F06008EED0A /* BlogDashboardPersonalizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */; }; 0CB4056C29C78F06008EED0A /* BlogDashboardPersonalizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */; }; 0CB4056E29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056D29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift */; }; @@ -6053,6 +6057,8 @@ 0C391E5D2A2FE5350040EA91 /* DashboardBlazeCampaignView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignView.swift; sourceTree = ""; }; 0C391E602A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignStatusView.swift; sourceTree = ""; }; 0C391E632A312DB20040EA91 /* DashboardBlazeCampaignViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignViewModelTests.swift; sourceTree = ""; }; + 0C60DF782A377C3800509C91 /* SiteSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingsView.swift; sourceTree = ""; }; + 0C60DF7D2A37C90200509C91 /* SiteSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingsViewModel.swift; sourceTree = ""; }; 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationService.swift; sourceTree = ""; }; 0CB4056D29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationServiceTests.swift; sourceTree = ""; }; 0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModel.swift; sourceTree = ""; }; @@ -10023,6 +10029,15 @@ path = Mocks; sourceTree = ""; }; + 0C60DF7C2A37C8E800509C91 /* Settings */ = { + isa = PBXGroup; + children = ( + 0C60DF782A377C3800509C91 /* SiteSettingsView.swift */, + 0C60DF7D2A37C90200509C91 /* SiteSettingsViewModel.swift */, + ); + path = Settings; + sourceTree = ""; + }; 0CB4056F29C8DCD7008EED0A /* BlogPersonalization */ = { isa = PBXGroup; children = ( @@ -14752,6 +14767,7 @@ FA5C74091C596E69000B528C /* Site Management */, FA73D7E72798766300DF24B3 /* Site Picker */, 3F43603823F36A76001DEE70 /* Site Settings */, + 0C60DF7C2A37C8E800509C91 /* Settings */, BE6AB7FB1BC62E0B00D980FC /* Style */, ); path = Blog; @@ -21353,6 +21369,7 @@ E64384831C628FCC0052ADB5 /* WPStyleGuide+Sharing.swift in Sources */, F504D2B025D60C5900A2764C /* StoryPoster.swift in Sources */, 981C82B62193A7B900A06E84 /* Double+Stats.swift in Sources */, + 0C60DF792A377C3800509C91 /* SiteSettingsView.swift in Sources */, 175507B327A062980038ED28 /* PublicizeConnectionURLMatcher.swift in Sources */, 177074851FB209F100951A4A /* CircularProgressView.swift in Sources */, 3F8B45A9292C1F2C00730FA4 /* DashboardMigrationSuccessCell.swift in Sources */, @@ -21565,6 +21582,7 @@ C81CCD81243BF7A600A83E27 /* TenorService.swift in Sources */, 8B51844525893F140085488D /* FilterBarView.swift in Sources */, E105205B1F2B1CF400A948F6 /* BlogToBlogMigration_61_62.swift in Sources */, + 0C60DF7E2A37C90200509C91 /* SiteSettingsViewModel.swift in Sources */, FE3E83E526A58646008CE851 /* ListSimpleOverlayView.swift in Sources */, 98B88452261E4E09007ED7F8 /* LikeUserTableViewCell.swift in Sources */, E16FB7E31F8B61040004DD9F /* WebKitViewController.swift in Sources */, @@ -23772,6 +23790,7 @@ FABB20F02602FC2C00C8785C /* ReaderDetailToolbar.swift in Sources */, 80A2154429D1177A002FE8EB /* RemoteConfigDebugViewController.swift in Sources */, FAD1263D2A0CF2F50004E24C /* String+NonbreakingSpace.swift in Sources */, + 0C60DF7A2A377C3800509C91 /* SiteSettingsView.swift in Sources */, FABB20F12602FC2C00C8785C /* RecentSitesService.swift in Sources */, FA332AD129C1F97A00182FBB /* MovedToJetpackViewController.swift in Sources */, 8BD66ED52787530C00CCD95A /* PostsCardViewModel.swift in Sources */, @@ -24875,6 +24894,7 @@ FABB241A2602FC2C00C8785C /* PostToPost30To31.m in Sources */, FABB241B2602FC2C00C8785C /* GutenGhostView.swift in Sources */, FABB241C2602FC2C00C8785C /* ModelSettableCell.swift in Sources */, + 0C60DF7F2A37C90200509C91 /* SiteSettingsViewModel.swift in Sources */, FABB241D2602FC2C00C8785C /* FollowCommentsService.swift in Sources */, 4AA33F022999D11A005B6E23 /* ReaderSiteTopic+Lookup.swift in Sources */, FABB241E2602FC2C00C8785C /* ForcePopoverPresenter.swift in Sources */,