diff --git a/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView+SheetModifier.swift b/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView+SheetModifier.swift new file mode 100644 index 00000000..c8b1826e --- /dev/null +++ b/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView+SheetModifier.swift @@ -0,0 +1,247 @@ +// +// UserProfileView+SheetModifier.swift +// Subconscious +// +// Created by Ben Follington on 14/8/2023. +// + +import Foundation +import SwiftUI +import ObservableStore + +struct MetaSheetModifier: ViewModifier { + let state: UserProfileDetailModel + let send: (UserProfileDetailAction) -> Void + + func body(content: Content) -> some View { + content + .sheet( + isPresented: Binding( + get: { state.isMetaSheetPresented }, + send: send, + tag: UserProfileDetailAction.presentMetaSheet + ) + ) { + UserProfileDetailMetaSheet( + state: state.metaSheet, + profile: state, + send: Address.forward( + send: send, + tag: UserProfileDetailMetaSheetCursor.tag + ) + ) + } + } +} + +struct FollowSheetModifier: ViewModifier { + let state: UserProfileDetailModel + let send: (UserProfileDetailAction) -> Void + + func body(content: Content) -> some View { + content + .sheet( + isPresented: Binding( + get: { state.isFollowSheetPresented }, + send: send, + tag: UserProfileDetailAction.presentFollowSheet + ) + ) { + FollowUserSheet( + state: state.followUserSheet, + send: Address.forward( + send: send, + tag: FollowUserSheetCursor.tag + ), + onAttemptFollow: { + let form = state.followUserSheet.followUserForm + guard let did = form.did.validated else { + return + } + guard let name = form.petname.validated else { + return + } + + send(.attemptFollow(did, name.toPetname())) + }, + label: Text("Follow"), + failFollowError: state.failFollowErrorMessage, + onDismissError: { + send(.dismissFailFollowError) + } + ) + } + } +} + +struct RenameSheetModifier: ViewModifier { + let state: UserProfileDetailModel + let send: (UserProfileDetailAction) -> Void + + func body(content: Content) -> some View { + content + .sheet( + isPresented: Binding( + get: { state.isRenameSheetPresented }, + send: send, + tag: UserProfileDetailAction.presentRenameSheet + ) + ) { + FollowUserSheet( + state: state.followUserSheet, + send: Address.forward( + send: send, + tag: FollowUserSheetCursor.tag + ), + onAttemptFollow: { + let form = state.followUserSheet.followUserForm + guard let name = form.petname.validated else { + return + } + guard let candidate = state.renameCandidate else { + return + } + + send(.attemptRename(from: candidate, to: name.toPetname())) + }, + label: Text("Rename"), + failFollowError: state.failRenameMessage, + onDismissError: { + send(.dismissFailRenameErrorMessage) + } + ) + } + } +} + +struct UnfollowSheetModifier: ViewModifier { + let state: UserProfileDetailModel + let send: (UserProfileDetailAction) -> Void + + func body(content: Content) -> some View { + content + .alert( + isPresented: Binding( + get: { state.failUnfollowErrorMessage != nil }, + set: { _ in send(.dismissFailUnfollowError) } + ) + ) { + Alert( + title: Text("Failed to Unfollow User"), + message: Text(state.failUnfollowErrorMessage ?? "An unknown error occurred") + ) + } + .confirmationDialog( + "Are you sure?", + isPresented: + Binding( + get: { state.isUnfollowConfirmationPresented }, + set: { _ in send(.presentUnfollowConfirmation(false)) } + ) + ) { + Button( + "Unfollow \(state.unfollowCandidate?.displayName ?? "user")?", + role: .destructive + ) { + send(.attemptUnfollow) + } + } message: { + Text("You cannot undo this action") + } + } +} + +struct EditProfileSheetModifier: ViewModifier { + @ObservedObject var app: Store + @ObservedObject var store: Store + private var state: UserProfileDetailModel { + store.state + } + private var send: (UserProfileDetailAction) -> Void { + store.send + } + + func body(content: Content) -> some View { + content + .sheet( + isPresented: Binding( + get: { state.isEditProfileSheetPresented }, + send: send, + tag: UserProfileDetailAction.presentEditProfile + ) + ) { + if let user = state.user { + EditProfileSheet( + state: state.editProfileSheet, + send: Address.forward( + send: send, + tag: EditProfileSheetCursor.tag + ), + user: user, + statistics: state.statistics, + failEditProfileMessage: state.failEditProfileMessage, + onEditProfile: { + send(.requestEditProfile) + }, + onCancel: { + send(.presentEditProfile(false)) + }, + onDismissError: { + send(.dismissEditProfileError) + } + ) + } + } + .onReceive( + store.actions.compactMap(AppAction.from), + perform: app.send + ) + .onReceive(app.actions, perform: { action in + switch (action) { + case .succeedIndexPeer(let peer) where peer.identity == store.state.user?.did: + store.send(.refresh(forceSync: false)) + break + case _: + break + } + }) + } +} + +struct FollowNewUserSheetModifier: ViewModifier { + let state: UserProfileDetailModel + let send: (UserProfileDetailAction) -> Void + + func body(content: Content) -> some View { + content + .sheet( + isPresented: Binding( + get: { state.isFollowNewUserFormSheetPresented }, + send: send, + tag: UserProfileDetailAction.presentFollowNewUserFormSheet + ) + ) { + FollowNewUserFormSheetView( + state: state.followNewUserFormSheet, + send: Address.forward( + send: send, + tag: FollowNewUserFormSheetCursor.tag + ), + did: state.user?.did, + onAttemptFollow: { + let form = state.followNewUserFormSheet.form + guard let did = form.did.validated else { + return + } + guard let name = form.petname.validated else { + return + } + + send(.attemptFollow(did, name.toPetname())) + }, + onCancel: { send(.presentFollowNewUserFormSheet(false)) }, + onDismissFailFollowError: { send(.dismissFailFollowError) } + ) + } + } +} diff --git a/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView.swift b/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView.swift index 91a5955d..1151d9f1 100644 --- a/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView.swift +++ b/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView.swift @@ -264,21 +264,21 @@ private extension View { state: UserProfileDetailModel, send: @escaping (UserProfileDetailAction) -> Void ) -> some View { - self.modifier(UnfollowModifier(state: state, send: send)) + self.modifier(UnfollowSheetModifier(state: state, send: send)) } func follow( state: UserProfileDetailModel, send: @escaping (UserProfileDetailAction) -> Void ) -> some View { - self.modifier(FollowModifier(state: state, send: send)) + self.modifier(FollowSheetModifier(state: state, send: send)) } func rename( state: UserProfileDetailModel, send: @escaping (UserProfileDetailAction) -> Void ) -> some View { - self.modifier(RenameModifier(state: state, send: send)) + self.modifier(RenameSheetModifier(state: state, send: send)) } func metaSheet( @@ -303,243 +303,6 @@ private extension View { } } -private struct MetaSheetModifier: ViewModifier { - let state: UserProfileDetailModel - let send: (UserProfileDetailAction) -> Void - - func body(content: Content) -> some View { - content - .sheet( - isPresented: Binding( - get: { state.isMetaSheetPresented }, - send: send, - tag: UserProfileDetailAction.presentMetaSheet - ) - ) { - UserProfileDetailMetaSheet( - state: state.metaSheet, - profile: state, - send: Address.forward( - send: send, - tag: UserProfileDetailMetaSheetCursor.tag - ) - ) - } - } -} - -private struct FollowModifier: ViewModifier { - let state: UserProfileDetailModel - let send: (UserProfileDetailAction) -> Void - - func body(content: Content) -> some View { - content - .sheet( - isPresented: Binding( - get: { state.isFollowSheetPresented }, - send: send, - tag: UserProfileDetailAction.presentFollowSheet - ) - ) { - FollowUserSheet( - state: state.followUserSheet, - send: Address.forward( - send: send, - tag: FollowUserSheetCursor.tag - ), - onAttemptFollow: { - let form = state.followUserSheet.followUserForm - guard let did = form.did.validated else { - return - } - guard let name = form.petname.validated else { - return - } - - send(.attemptFollow(did, name.toPetname())) - }, - label: Text("Follow"), - failFollowError: state.failFollowErrorMessage, - onDismissError: { - send(.dismissFailFollowError) - } - ) - } - } -} - -private struct RenameModifier: ViewModifier { - let state: UserProfileDetailModel - let send: (UserProfileDetailAction) -> Void - - func body(content: Content) -> some View { - content - .sheet( - isPresented: Binding( - get: { state.isRenameSheetPresented }, - send: send, - tag: UserProfileDetailAction.presentRenameSheet - ) - ) { - FollowUserSheet( - state: state.followUserSheet, - send: Address.forward( - send: send, - tag: FollowUserSheetCursor.tag - ), - onAttemptFollow: { - let form = state.followUserSheet.followUserForm - guard let name = form.petname.validated else { - return - } - guard let candidate = state.renameCandidate else { - return - } - - send(.attemptRename(from: candidate, to: name.toPetname())) - }, - label: Text("Rename"), - failFollowError: state.failRenameMessage, - onDismissError: { - send(.dismissFailRenameErrorMessage) - } - ) - } - } -} - -private struct UnfollowModifier: ViewModifier { - let state: UserProfileDetailModel - let send: (UserProfileDetailAction) -> Void - - func body(content: Content) -> some View { - content - .alert( - isPresented: Binding( - get: { state.failUnfollowErrorMessage != nil }, - set: { _ in send(.dismissFailUnfollowError) } - ) - ) { - Alert( - title: Text("Failed to Unfollow User"), - message: Text(state.failUnfollowErrorMessage ?? "An unknown error occurred") - ) - } - .confirmationDialog( - "Are you sure?", - isPresented: - Binding( - get: { state.isUnfollowConfirmationPresented }, - set: { _ in send(.presentUnfollowConfirmation(false)) } - ) - ) { - Button( - "Unfollow \(state.unfollowCandidate?.displayName ?? "user")?", - role: .destructive - ) { - send(.attemptUnfollow) - } - } message: { - Text("You cannot undo this action") - } - } -} - -private struct EditProfileSheetModifier: ViewModifier { - @ObservedObject var app: Store - @ObservedObject var store: Store - private var state: UserProfileDetailModel { - store.state - } - private var send: (UserProfileDetailAction) -> Void { - store.send - } - - func body(content: Content) -> some View { - content - .sheet( - isPresented: Binding( - get: { state.isEditProfileSheetPresented }, - send: send, - tag: UserProfileDetailAction.presentEditProfile - ) - ) { - if let user = state.user { - EditProfileSheet( - state: state.editProfileSheet, - send: Address.forward( - send: send, - tag: EditProfileSheetCursor.tag - ), - user: user, - statistics: state.statistics, - failEditProfileMessage: state.failEditProfileMessage, - onEditProfile: { - send(.requestEditProfile) - }, - onCancel: { - send(.presentEditProfile(false)) - }, - onDismissError: { - send(.dismissEditProfileError) - } - ) - } - } - .onReceive( - store.actions.compactMap(AppAction.from), - perform: app.send - ) - .onReceive(app.actions, perform: { action in - switch (action) { - case .succeedIndexPeer(let peer) where peer.identity == store.state.user?.did: - store.send(.refresh(forceSync: false)) - break - case _: - break - } - }) - } -} - -private struct FollowNewUserSheetModifier: ViewModifier { - let state: UserProfileDetailModel - let send: (UserProfileDetailAction) -> Void - - func body(content: Content) -> some View { - content - .sheet( - isPresented: Binding( - get: { state.isFollowNewUserFormSheetPresented }, - send: send, - tag: UserProfileDetailAction.presentFollowNewUserFormSheet - ) - ) { - FollowNewUserFormSheetView( - state: state.followNewUserFormSheet, - send: Address.forward( - send: send, - tag: FollowNewUserFormSheetCursor.tag - ), - did: state.user?.did, - onAttemptFollow: { - let form = state.followNewUserFormSheet.form - guard let did = form.did.validated else { - return - } - guard let name = form.petname.validated else { - return - } - - send(.attemptFollow(did, name.toPetname())) - }, - onCancel: { send(.presentFollowNewUserFormSheet(false)) }, - onDismissFailFollowError: { send(.dismissFailFollowError) } - ) - } - } -} - struct UserProfileView_Previews: PreviewProvider { static var previews: some View { UserProfileView( diff --git a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj index e1e64ef4..cb1f7c66 100644 --- a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj +++ b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj @@ -67,6 +67,8 @@ B59D556329BBFF56007915E2 /* FormField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59D556229BBFF56007915E2 /* FormField.swift */; }; B5A7AD322A0D0B0E007C3535 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A7AD312A0D0B0E007C3535 /* EmptyStateView.swift */; }; B5A7AD332A0D0B0E007C3535 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A7AD312A0D0B0E007C3535 /* EmptyStateView.swift */; }; + B5BBA7352A89E20300864220 /* UserProfileView+SheetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BBA7342A89E20300864220 /* UserProfileView+SheetModifier.swift */; }; + B5BBA7362A89E20300864220 /* UserProfileView+SheetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BBA7342A89E20300864220 /* UserProfileView+SheetModifier.swift */; }; B5C918EE2A67A16A004C6CD5 /* AuthorizationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C918ED2A67A16A004C6CD5 /* AuthorizationSettingsView.swift */; }; B5C918F02A67ADEF004C6CD5 /* SphereSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C918EF2A67ADEF004C6CD5 /* SphereSettingsView.swift */; }; B5C918F62A67BB35004C6CD5 /* EllipsisLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C918F52A67BB35004C6CD5 /* EllipsisLabelView.swift */; }; @@ -510,6 +512,7 @@ B5908BEA29DAB05B00225B1A /* TestUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtilities.swift; sourceTree = ""; }; B59D556229BBFF56007915E2 /* FormField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormField.swift; sourceTree = ""; }; B5A7AD312A0D0B0E007C3535 /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = ""; }; + B5BBA7342A89E20300864220 /* UserProfileView+SheetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProfileView+SheetModifier.swift"; sourceTree = ""; }; B5C918ED2A67A16A004C6CD5 /* AuthorizationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSettingsView.swift; sourceTree = ""; }; B5C918EF2A67ADEF004C6CD5 /* SphereSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SphereSettingsView.swift; sourceTree = ""; }; B5C918F52A67BB35004C6CD5 /* EllipsisLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EllipsisLabelView.swift; sourceTree = ""; }; @@ -865,6 +868,7 @@ B58C1FB92A12F8DE0085A1FE /* ProfilePicFrameViewModifier.swift */, B58A46B629D3D09500491E43 /* UserProfileHeaderView.swift */, B58C73A629DBB3B500B00EA1 /* UserProfileView.swift */, + B5BBA7342A89E20300864220 /* UserProfileView+SheetModifier.swift */, ); path = Profile; sourceTree = ""; @@ -1808,6 +1812,7 @@ B88A91A12A4C9C0100422ABF /* EntryListEmptyView.swift in Sources */, B58FD6732A4E4C0E00826548 /* InviteCodeSettingsSection.swift in Sources */, B5F6ADC929C02F4A00690DE4 /* AddressBookService.swift in Sources */, + B5BBA7352A89E20300864220 /* UserProfileView+SheetModifier.swift in Sources */, B8925B2F29C23017001F9503 /* MemoViewerDetailView.swift in Sources */, B56C3D3E2A01E5020071EF70 /* InviteCode.swift in Sources */, B51EEAA129F0C37B0055887B /* AppIcon.swift in Sources */, @@ -2017,6 +2022,7 @@ B8DEBF232798EE99007CB528 /* RenameSuggestionLabelView.swift in Sources */, B8A41D522811F87C0096D2E7 /* SlashlinkBarView.swift in Sources */, B866868B27AC8BED00A03A55 /* DetailKeyboardToolbarView.swift in Sources */, + B5BBA7362A89E20300864220 /* UserProfileView+SheetModifier.swift in Sources */, B5FB9D9529D51D9600D64988 /* UserProfileHeaderView.swift in Sources */, B57701972A650C92001F874F /* ProfileHeaderButtonStyle.swift in Sources */, B8133AEC29B8FD1300B38760 /* SubtextAttributedStringRenderer.swift in Sources */,