diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 73ca7ddc1..6c6306ec4 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -365,6 +365,8 @@ CDF842682A49FB9000723DA0 /* Inbox View Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF842672A49FB9000723DA0 /* Inbox View Logic.swift */; }; CDF8426B2A4A2AB600723DA0 /* Inbox Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8426A2A4A2AB600723DA0 /* Inbox Item.swift */; }; CDF8426F2A4A385A00723DA0 /* Inbox Item Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8426E2A4A385A00723DA0 /* Inbox Item Type.swift */; }; + E4D4DBA02A7C7B9D00C4F3DE /* Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D4DB9F2A7C7B9D00C4F3DE /* Comments.swift */; }; + E4DDB4322A81819300B3A7E0 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4DDB4312A81819300B3A7E0 /* Double.swift */; }; E453A1D02A81C2140004BB8A /* QuickLookPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E453A1CF2A81C2140004BB8A /* QuickLookPreviewController.swift */; }; E4DDB4342A819C8000B3A7E0 /* QuickLookView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4DDB4332A819C8000B3A7E0 /* QuickLookView.swift */; }; /* End PBXBuildFile section */ @@ -743,6 +745,8 @@ CDF842672A49FB9000723DA0 /* Inbox View Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox View Logic.swift"; sourceTree = ""; }; CDF8426A2A4A2AB600723DA0 /* Inbox Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox Item.swift"; sourceTree = ""; }; CDF8426E2A4A385A00723DA0 /* Inbox Item Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox Item Type.swift"; sourceTree = ""; }; + E4D4DB9F2A7C7B9D00C4F3DE /* Comments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comments.swift; sourceTree = ""; }; + E4DDB4312A81819300B3A7E0 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; E453A1CF2A81C2140004BB8A /* QuickLookPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreviewController.swift; sourceTree = ""; }; E4DDB4332A819C8000B3A7E0 /* QuickLookView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1076,6 +1080,7 @@ isa = PBXGroup; children = ( 6D8003782A45FD1300363206 /* Bundle.swift */, + E4DDB4312A81819300B3A7E0 /* Double.swift */, 6332FDC227EFCB5F0009A98A /* Color.swift */, 6386E0392A0455BC006B3C1D /* String - Contains Elements From Array.swift */, 63F0C7BA2A058CB700A18C5D /* URLSessionWebSocketTask - Send Ping.swift */, @@ -1197,6 +1202,7 @@ 6363D5C327EE196700E34822 /* Mlem */ = { isa = PBXGroup; children = ( + E4D4DB9E2A7C7A5800C4F3DE /* Animations */, CDE9CE4A2A7B07F6002B97DD /* Haptics */, CDC6A8C82A6F1C8000CC11AC /* Protocols */, CDDCF6412A662444003DA3AC /* Custom Tab Bar */, @@ -1926,6 +1932,14 @@ path = Messages; sourceTree = ""; }; + E4D4DB9E2A7C7A5800C4F3DE /* Animations */ = { + isa = PBXGroup; + children = ( + E4D4DB9F2A7C7B9D00C4F3DE /* Comments.swift */, + ); + path = Animations; + sourceTree = ""; + }; E453A1CE2A81C1F20004BB8A /* Quick Look */ = { isa = PBXGroup; children = ( @@ -2171,6 +2185,7 @@ B1DD00BD2A62DDEC002A7B39 /* RecognizedLemmyInstances.swift in Sources */, 6DA61F892A575DF1001EA633 /* URL - Lemmy Image Parameters.swift in Sources */, ADDC9E3C2A5CF02A00383D58 /* BlockPersonLogic.swift in Sources */, + E4DDB4322A81819300B3A7E0 /* Double.swift in Sources */, CD3FBCD92A4A6BD100B2063F /* Replies Tracker.swift in Sources */, 5016A2B32A67EC0700B257E8 /* NotificationDisplayer.swift in Sources */, CD1446212A5B328E00610EF1 /* Privacy Policy.swift in Sources */, @@ -2369,6 +2384,7 @@ 6386E0402A045723006B3C1D /* Website Icon Complex.swift in Sources */, 5064D0412A6E63E000B22EE3 /* Task+Notifiable.swift in Sources */, 63F0C7BD2A058CD200A18C5D /* Check if Endpoint Exists.swift in Sources */, + E4D4DBA02A7C7B9D00C4F3DE /* Comments.swift in Sources */, 6363D5C727EE196700E34822 /* ContentView.swift in Sources */, 6D8F08FF2A4029AE003EB4FD /* Community List View.swift in Sources */, 5016A2B12A67EB8600B257E8 /* UIViewController.swift in Sources */, diff --git a/Mlem/Animations/Comments.swift b/Mlem/Animations/Comments.swift new file mode 100644 index 000000000..3aca3b20e --- /dev/null +++ b/Mlem/Animations/Comments.swift @@ -0,0 +1,32 @@ +// +// Comments.swift +// Mlem +// +// Created by Bosco Ho on 2023-08-03. +// + +import SwiftUI + +internal extension Animation { + + /// Animation for expanding or collapsing a comment and its child comments. + static func showHideComment(_ collapse: Bool) -> Animation { + let standard = (0.4, 1.0, collapse ? 0.25 : 0.3) + let animationValues = standard + return .interactiveSpring( + response: animationValues.0, + dampingFraction: animationValues.1, + blendDuration: animationValues.2) + } +} + +internal extension AnyTransition { + + static func markdownView() -> AnyTransition { + .opacity + } + + static func commentView() -> AnyTransition { + .move(edge: .top).combined(with: .opacity) + } +} diff --git a/Mlem/Extensions/Double.swift b/Mlem/Extensions/Double.swift new file mode 100644 index 000000000..ae7adfe88 --- /dev/null +++ b/Mlem/Extensions/Double.swift @@ -0,0 +1,20 @@ +// +// Double.swift +// Mlem +// +// Created by Bosco Ho on 2023-08-07. +// + +import Foundation + +// MARK: - SwiftUI +extension Double { + + /// Use this value in SwiftUI to modify a view to be the top-most layer. + /// + /// This sentinel value exists because using `Int.max` doesn't work. + static var maxZIndex: Double { + /// [2023.08] `Int.max` doesn't work, which is why this is set to just some big value. + return 99999 + } +} diff --git a/Mlem/Views/Shared/Comments/Comment Item.swift b/Mlem/Views/Shared/Comments/Comment Item.swift index 6e3cdaee8..d955c61fa 100644 --- a/Mlem/Views/Shared/Comments/Comment Item.swift +++ b/Mlem/Views/Shared/Comments/Comment Item.swift @@ -95,65 +95,62 @@ struct CommentItem: View { } // MARK: Body - + // swiftlint:disable line_length var body: some View { - if hierarchicalComment.isParentCollapsed, hierarchicalComment.isCollapsed, hierarchicalComment.commentView.comment.parentId != nil { - EmptyView() - } else if hierarchicalComment.isParentCollapsed, !hierarchicalComment.isCollapsed, hierarchicalComment.commentView.comment.parentId != nil { - EmptyView() - } else { + Group { VStack(spacing: 0) { - commentBody(hierarchicalComment: self.hierarchicalComment) - Divider() + if hierarchicalComment.isParentCollapsed, hierarchicalComment.isCollapsed, hierarchicalComment.commentView.comment.parentId != nil { + EmptyView() + } else if hierarchicalComment.isParentCollapsed, !hierarchicalComment.isCollapsed, hierarchicalComment.commentView.comment.parentId != nil { + EmptyView() + } else { + Group { + commentBody(hierarchicalComment: self.hierarchicalComment) + Divider() + } + /// Clips any transitions to this view, otherwise comments will animate over other ones. + .clipped() + .padding(.leading, indentValue) + .transition(.commentView()) + } } - .clipped() - .padding(.leading, indentValue) } } // swiftlint:enable line_length // MARK: Subviews - // swiftlint:disable function_body_length @ViewBuilder private func commentBody(hierarchicalComment: HierarchicalComment) -> some View { - Group { - VStack(alignment: .leading, spacing: 0) { - CommentBodyView(commentView: hierarchicalComment.commentView, - isParentCollapsed: $hierarchicalComment.isParentCollapsed, - isCollapsed: $hierarchicalComment.isCollapsed, - showPostContext: showPostContext, - menuFunctions: genMenuFunctions()) - // top and bottom spacing uses default even when compact--it's *too* compact otherwise - .padding(.top, AppConstants.postAndCommentSpacing) - .padding(.horizontal, AppConstants.postAndCommentSpacing) - - if !hierarchicalComment.isCollapsed && !compactComments { - CommentInteractionBar(commentView: hierarchicalComment.commentView, - displayedScore: displayedScore, - displayedVote: displayedVote, - displayedSaved: displayedSaved, - upvote: upvote, - downvote: downvote, - saveComment: saveComment, - deleteComment: deleteComment, - replyToComment: replyToComment) - } else { - Spacer() - .frame(height: AppConstants.postAndCommentSpacing) - } + VStack(alignment: .leading, spacing: 0) { + CommentBodyView(commentView: hierarchicalComment.commentView, + isParentCollapsed: $hierarchicalComment.isParentCollapsed, + isCollapsed: $hierarchicalComment.isCollapsed, + showPostContext: showPostContext, + menuFunctions: genMenuFunctions()) + // top and bottom spacing uses default even when compact--it's *too* compact otherwise + .padding(.top, AppConstants.postAndCommentSpacing) + .padding(.horizontal, AppConstants.postAndCommentSpacing) + + if !hierarchicalComment.isCollapsed && !compactComments { + CommentInteractionBar(commentView: hierarchicalComment.commentView, + displayedScore: displayedScore, + displayedVote: displayedVote, + displayedSaved: displayedSaved, + upvote: upvote, + downvote: downvote, + saveComment: saveComment, + deleteComment: deleteComment, + replyToComment: replyToComment) + } else { + Spacer() + .frame(height: AppConstants.postAndCommentSpacing) } - .transition( - .asymmetric( - insertion: .move(edge: .bottom).combined(with: .opacity), - removal: .move(edge: .top).combined(with: .opacity) - ) - ) } .contentShape(Rectangle()) // allow taps in blank space to register .onTapGesture { - withAnimation(.interactiveSpring(response: 0.4, dampingFraction: 1, blendDuration: 0.4)) { + withAnimation(.showHideComment(!hierarchicalComment.isCollapsed)) { // Perhaps we want an explict flag for this in the future? if !showPostContext { commentTracker.setCollapsed(!hierarchicalComment.isCollapsed, comment: hierarchicalComment) @@ -182,7 +179,6 @@ struct CommentItem: View { // report: true)) // } } - // swiftlint:enable function_body_length } // MARK: - Swipe Actions diff --git a/Mlem/Views/Shared/Comments/Components/CommentBodyView.swift b/Mlem/Views/Shared/Comments/Components/CommentBodyView.swift index 3e46552f5..f4b62977f 100644 --- a/Mlem/Views/Shared/Comments/Components/CommentBodyView.swift +++ b/Mlem/Views/Shared/Comments/Components/CommentBodyView.swift @@ -87,6 +87,7 @@ struct CommentBodyView: View { } else if !isCollapsed { MarkdownView(text: commentView.comment.content, isNsfw: commentView.post.nsfw) .frame(maxWidth: .infinity, alignment: .topLeading) + .transition(.markdownView()) } } diff --git a/Mlem/Views/Shared/Posts/Expanded Post.swift b/Mlem/Views/Shared/Posts/Expanded Post.swift index 9ee4f002d..03b6e1b21 100644 --- a/Mlem/Views/Shared/Posts/Expanded Post.swift +++ b/Mlem/Views/Shared/Posts/Expanded Post.swift @@ -162,6 +162,8 @@ struct ExpandedPost: View { showPostContext: false, showCommentCreator: true ) + /// [2023.08] Manually set zIndex so child comments don't overlap parent comments on collapse/expand animations. `Int.max` doesn't work, which is why this is set to just some big value. + .zIndex(.maxZIndex - Double(comment.depth)) } } }