diff --git a/Source/Hollow.xcodeproj/project.pbxproj b/Source/Hollow.xcodeproj/project.pbxproj index d1889577..6c6fb8ed 100644 --- a/Source/Hollow.xcodeproj/project.pbxproj +++ b/Source/Hollow.xcodeproj/project.pbxproj @@ -2080,7 +2080,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hollow/Hollow.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = C5UH93T368; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Hollow/Info.plist; @@ -2106,7 +2106,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hollow/Hollow.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = C5UH93T368; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Hollow/Info.plist; @@ -2131,7 +2131,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = HollowWidget/HollowWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = C5UH93T368; INFOPLIST_FILE = HollowWidget/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; @@ -2159,7 +2159,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = HollowWidget/HollowWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = C5UH93T368; INFOPLIST_FILE = HollowWidget/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; diff --git a/Source/Hollow/App/AppModel.swift b/Source/Hollow/App/AppModel.swift index e9819658..55dd4152 100644 --- a/Source/Hollow/App/AppModel.swift +++ b/Source/Hollow/App/AppModel.swift @@ -16,6 +16,7 @@ class AppModel: ObservableObject { private var cancellables = Set() @Published var isInMainView = Defaults[.accessToken] != nil && Defaults[.hollowConfig] != nil + var widgetReloadCount = 0 private init() { // Chcek for version update diff --git a/Source/Hollow/App/HollowApp.swift b/Source/Hollow/App/HollowApp.swift index b2d8d81e..e491336b 100644 --- a/Source/Hollow/App/HollowApp.swift +++ b/Source/Hollow/App/HollowApp.swift @@ -44,7 +44,10 @@ struct HollowApp: App { NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), perform: { _ in appDelegate.fetchConfig() - WidgetCenter.shared.reloadAllTimelines() + if appModel.widgetReloadCount % 3 == 0 { + WidgetCenter.shared.reloadAllTimelines() + appModel.widgetReloadCount += 1 + } } ) .onOpenURL { url in diff --git a/Source/Hollow/View/Hierarchy/Hollow/Content/HollowContentView.swift b/Source/Hollow/View/Hierarchy/Hollow/Content/HollowContentView.swift index e46a9677..02c8a535 100644 --- a/Source/Hollow/View/Hierarchy/Hollow/Content/HollowContentView.swift +++ b/Source/Hollow/View/Hierarchy/Hollow/Content/HollowContentView.swift @@ -114,7 +114,7 @@ struct HollowContentView: View { options.contains(.replaceForImageOnly) { text = "[" + NSLocalizedString("TEXTVIEW_PHOTO_PLACEHOLDER_TEXT", comment: "") + "]" } - return HollowTextView(postData: postDataWrapper.post, inDetail: !options.contains(.compactText), highlight: postDataWrapper.post.renderHighlight && options.contains(.showHyperlinks), compactLineLimit: options.contains(.compactText) ? lineLimit : nil) + return HollowTextView(postData: postDataWrapper.post, inDetail: !options.contains(.compactText), highlight: postDataWrapper.post.renderHighlight && options.contains(.showHyperlinks), links: postDataWrapper.post.url, citedNumbers: postDataWrapper.post.citedNumbers, compactLineLimit: options.contains(.compactText) ? lineLimit : nil) } private func tagView(text: String, deleted: Bool) -> some View { diff --git a/Source/Hollow/View/Hierarchy/Hollow/Content/HollowTextView.swift b/Source/Hollow/View/Hierarchy/Hollow/Content/HollowTextView.swift index aec304b4..8a2a125c 100644 --- a/Source/Hollow/View/Hierarchy/Hollow/Content/HollowTextView.swift +++ b/Source/Hollow/View/Hierarchy/Hollow/Content/HollowTextView.swift @@ -12,28 +12,28 @@ import Introspect struct HollowTextView: View { var text: String - var hasURL: Bool - var hasCitedNumbers: Bool var inDetail: Bool var highlight: Bool + var links: [String] = [] + var citedNumbers: [Int] = [] var compactLineLimit: Int? = nil - init(postData: PostData, inDetail: Bool, highlight: Bool, compactLineLimit: Int? = nil) { + init(postData: PostData, inDetail: Bool, highlight: Bool, links: [String], citedNumbers: [Int], compactLineLimit: Int? = nil) { self.text = postData.text - self.hasURL = postData.hasURL - self.hasCitedNumbers = postData.hasCitedNumbers self.inDetail = inDetail self.highlight = highlight + self.links = links + self.citedNumbers = citedNumbers self.compactLineLimit = compactLineLimit } init(text: String, hasURL: Bool = true, hasCitedNumbers: Bool = true, inDetail: Bool, highlight: Bool, compactLineLimit: Int? = nil) { self.text = text - self.hasURL = hasURL - self.hasCitedNumbers = hasCitedNumbers self.inDetail = inDetail self.highlight = highlight + self.links = text.links() + self.citedNumbers = text.citationNumbers() self.compactLineLimit = compactLineLimit } @@ -59,13 +59,13 @@ struct HollowTextView: View { $0.underline() .foregroundColor(.hollowContentText) }) - + } else { Text(text) - + } } - + .modifier(TextModifier(inDetail: inDetail, compactLineLimit: compactLineLimit)) } @@ -78,34 +78,31 @@ struct HollowTextView: View { }) Divider() } - if hasURL { - let links = Array(text.links().compactMap({ URL(string: $0) })) - if !links.isEmpty { - Divider() - ForEach(links, id: \.self) { link in - Button(action: { - let helper = OpenURLHelper(openURL: openURL) - try? helper.tryOpen(link, method: Defaults[.openURLMethod]) - }) { - Label(link.absoluteString, systemImage: "link") - } + let links = Array(links.compactMap({ URL(string: $0) })) + if !links.isEmpty { + Divider() + ForEach(links, id: \.self) { link in + Button(action: { + let helper = OpenURLHelper(openURL: openURL) + try? helper.tryOpen(link, method: Defaults[.openURLMethod]) + }) { + Label(link.absoluteString, systemImage: "link") } - Divider() } + Divider() } - if hasCitedNumbers { - let citedPosts = text.citationNumbers() - if !citedPosts.isEmpty { - ForEach(citedPosts, id: \.self) { post in - let wrapper = PostDataWrapper.templatePost(for: post) - Button(action: { - IntegrationUtilities.conditionallyPresentDetail(store: .init(bindingPostWrapper: .constant(wrapper))) - }) { - Label("#\(post.string)", systemImage: "text.quote") - } + + let citedPosts = citedNumbers + if !citedPosts.isEmpty { + ForEach(citedPosts, id: \.self) { post in + let wrapper = PostDataWrapper.templatePost(for: post) + Button(action: { + IntegrationUtilities.conditionallyPresentDetail(store: .init(bindingPostWrapper: .constant(wrapper))) + }) { + Label("#\(post.string)", systemImage: "text.quote") } - Divider() } + Divider() } } diff --git a/Source/Hollow/View/Hierarchy/Hollow/Detail/HollowDetailSubViews.swift b/Source/Hollow/View/Hierarchy/Hollow/Detail/HollowDetailSubViews.swift index 48a05851..57f6591c 100644 --- a/Source/Hollow/View/Hierarchy/Hollow/Detail/HollowDetailSubViews.swift +++ b/Source/Hollow/View/Hierarchy/Hollow/Detail/HollowDetailSubViews.swift @@ -77,109 +77,99 @@ extension HollowDetailView { } func commentView(for comment: CommentData) -> some View { - var hideLabel: Bool = false - let index = store.postDataWrapper.post.comments.firstIndex(where: { $0.commentId == comment.commentId }) + let hideLabel: Bool = showOnlyName == comment.name ? + false : + (reverseComments ? !comment.showAvatarWhenReversed : !comment.showAvatar) - if showOnlyName != comment.name { - let firstIndex = reverseComments ? store.postDataWrapper.post.comments.count - 1 : 0 - if index == firstIndex { hideLabel = false } - else if let index = index { - let nextIndex = reverseComments ? index + 1 : index - 1 - hideLabel = comment.name == store.postDataWrapper.post.comments[nextIndex].name + let bindingComment = Binding( + get: { comment }, + set: { comment in + if let index = store.postDataWrapper.post.comments.firstIndex(where: { $0.commentId == comment.commentId }) { + store.postDataWrapper.post.comments[index] = comment + } } - } + ) - return Group { if let index = index { - let bindingComment = Binding( - get: { comment }, - set: { comment in - if let index = store.postDataWrapper.post.comments.firstIndex(where: { $0.commentId == comment.commentId }) { - store.postDataWrapper.post.comments[index] = comment - } - } - ) - - let highlighted = store.replyToIndex == index || jumpedToCommentId == comment.commentId + let highlighted = store.replyToId == comment.commentId || jumpedToCommentId == comment.commentId + + return HollowCommentContentView( + commentData: bindingComment, + compact: false, + contentVerticalPadding: UIDevice.isMac ? 13 : 10, + hideLabel: hideLabel, + postColorIndex: store.postDataWrapper.post.colorIndex, + postHash: store.postDataWrapper.post.hash, + imageReloadHandler: { store.reloadImage($0, commentId: comment.commentId) }, + jumpToReplyingHandler: { jumpToComment(commentId: comment.replyTo) } + ) + .padding(.horizontal) + .background( + Group { + highlighted ? Color.background : Color.hollowCardBackground + } + ) + .onClickGesture { + guard !store.isSendingComment && !store.isLoading else { return } + UIImpactFeedbackGenerator(style: .soft).impactOccurred() + withAnimation(scrollAnimation) { + store.replyToId = comment.commentId + jumpedToCommentId = nil + } + } + .contextMenu { + if comment.text != "" { + Button(action: { + UIPasteboard.general.string = comment.text + }, label: { + Label(NSLocalizedString("COMMENT_VIEW_COPY_TEXT_LABEL", comment: ""), systemImage: "doc.on.doc") + }) + } - HollowCommentContentView( - commentData: bindingComment, - compact: false, - contentVerticalPadding: UIDevice.isMac ? 13 : 10, - hideLabel: hideLabel, - postColorIndex: store.postDataWrapper.post.colorIndex, - postHash: store.postDataWrapper.post.hash, - imageReloadHandler: { store.reloadImage($0, commentId: comment.commentId) }, - jumpToReplyingHandler: { jumpToComment(commentId: comment.replyTo) } - ) - .padding(.horizontal) - .background( - Group { - highlighted ? Color.background : Color.hollowCardBackground - } - ) - .onClickGesture { - guard !store.isSendingComment && !store.isLoading else { return } - UIImpactFeedbackGenerator(style: .soft).impactOccurred() - withAnimation(scrollAnimation) { - store.replyToIndex = index - jumpedToCommentId = nil + if showOnlyName == nil { + Button(action: { withAnimation { showOnlyName = comment.name } }) { + Label("COMMENT_VIEW_SHOW_ONLY_LABEL", systemImage: "line.horizontal.3.decrease.circle") } + Divider() } - .contextMenu { - if comment.text != "" { + + let links = Array(comment.url.compactMap({ URL(string: $0) })) + if !links.isEmpty { + Divider() + ForEach(links, id: \.self) { link in Button(action: { - UIPasteboard.general.string = comment.text - }, label: { - Label(NSLocalizedString("COMMENT_VIEW_COPY_TEXT_LABEL", comment: ""), systemImage: "doc.on.doc") - }) - } - - if showOnlyName == nil { - Button(action: { withAnimation { showOnlyName = comment.name } }) { - Label("COMMENT_VIEW_SHOW_ONLY_LABEL", systemImage: "line.horizontal.3.decrease.circle") - } - Divider() - } - - if comment.hasURL { - let links = Array(comment.text.links().compactMap({ URL(string: $0) })) - Divider() - ForEach(links, id: \.self) { link in - Button(action: { - let helper = OpenURLHelper(openURL: openURL) - try? helper.tryOpen(link, method: Defaults[.openURLMethod]) - }) { - Label(link.absoluteString, systemImage: "link") - } + let helper = OpenURLHelper(openURL: openURL) + try? helper.tryOpen(link, method: Defaults[.openURLMethod]) + }) { + Label(link.absoluteString, systemImage: "link") } - Divider() } - if comment.hasCitedNumbers { - let citedPosts = comment.text.citationNumbers() - Divider() - ForEach(citedPosts, id: \.self) { post in - let wrapper = PostDataWrapper.templatePost(for: post) - Button(action: { - IntegrationUtilities.conditionallyPresentDetail(store: .init(bindingPostWrapper: .constant(wrapper))) - }) { - Label("#\(post.string)", systemImage: "text.quote") - } + Divider() + } + let citedPosts = comment.citedNumbers + if !citedPosts.isEmpty { + Divider() + ForEach(citedPosts, id: \.self) { post in + let wrapper = PostDataWrapper.templatePost(for: post) + Button(action: { + IntegrationUtilities.conditionallyPresentDetail(store: .init(bindingPostWrapper: .constant(wrapper))) + }) { + Label("#\(post.string)", systemImage: "text.quote") } - Divider() } - ReportMenuContent( - store: store, - permissions: comment.permissions, - commentId: comment.commentId - ) + Divider() } - }} - + ReportMenuContent( + store: store, + permissions: comment.permissions, + commentId: comment.commentId + ) + } + } func jumpToComment(commentId: Int) { - withAnimation(scrollAnimation) { store.replyToIndex = -2 } - jumpedFromCommentId = commentId + withAnimation(scrollAnimation) { store.replyToId = -2 } + withAnimation(scrollAnimation) { jumpedFromCommentId = commentId } } struct PlaceholderComment: View { diff --git a/Source/Hollow/View/Hierarchy/Hollow/Detail/HollowDetailView.swift b/Source/Hollow/View/Hierarchy/Hollow/Detail/HollowDetailView.swift index f00f8c49..4624fc5d 100644 --- a/Source/Hollow/View/Hierarchy/Hollow/Detail/HollowDetailView.swift +++ b/Source/Hollow/View/Hierarchy/Hollow/Detail/HollowDetailView.swift @@ -34,6 +34,8 @@ struct HollowDetailView: View { @Namespace var buttonAnimationNamespace + let hasTopSafeAreaInset = UIApplication.shared.windows[0].safeAreaInsets.top > 0 + var body: some View { VStack(spacing: 0) { if showHeader { @@ -131,9 +133,8 @@ struct HollowDetailView: View { .background(Color.hollowCardBackground) .coordinateSpace(name: "detail.scrollview") .buttonStyle(BorderlessButtonStyle()) - .onChange(of: store.replyToIndex) { index in - guard index != -2 else { return } - let id = index >= 0 ? store.postDataWrapper.post.comments[index].commentId : -1 + .onChange(of: store.replyToId) { id in + guard id != -2 else { return } withAnimation(scrollAnimation) { proxy.scrollTo(id, anchor: .top) } @@ -183,17 +184,16 @@ struct HollowDetailView: View { .proposedIgnoringSafeArea() ) - .overlay(Group { if store.replyToIndex < -1 && !store.noSuchPost { + .overlay(Group { if store.replyToId < -1 && !store.noSuchPost { FloatButton( action: { - withAnimation(scrollAnimation) { store.replyToIndex = -1 } + withAnimation(scrollAnimation) { store.replyToId = -1 } UIImpactFeedbackGenerator(style: .soft).impactOccurred() }, systemImageName: "text.bubble.fill", imageScaleFactor: 0.8, buttonAnimationNamespace: buttonAnimationNamespace ) - // .matchedGeometryEffect(id: "button", in: buttonAnimationNamespace) .proposedIgnoringSafeArea(edges: .bottom) .bottom() .trailing() @@ -204,11 +204,11 @@ struct HollowDetailView: View { }}) .proposedIgnoringSafeArea(edges: .bottom) - .overlay(Group { if store.replyToIndex >= -1 { + .overlay(Group { if store.replyToId >= -1 { let post = store.postDataWrapper.post - let name = store.replyToIndex == -1 ? + let name = store.replyToId == -1 ? NSLocalizedString("COMMENT_INPUT_REPLY_POST_SUFFIX", comment: "") : - post.comments[store.replyToIndex].name + post.comments.first(where: { $0.commentId == store.replyToId })?.name ?? "" HollowCommentInputView( store: store, buttonAnimationNamespace: UIDevice.isPad ? nil : buttonAnimationNamespace, @@ -221,13 +221,13 @@ struct HollowDetailView: View { }}) - .onChange(of: store.replyToIndex) { index in - if index >= -1 { + .onChange(of: store.replyToId) { id in + if id >= -1 { withAnimation(scrollAnimation) { inputPresented = true } } } .onChange(of: inputPresented) { presented in - if !presented { withAnimation(scrollAnimation) { store.replyToIndex = -2 }} + if !presented { withAnimation(scrollAnimation) { store.replyToId = -2 }} } .onDisappear { diff --git a/Source/Hollow/View/Hierarchy/Hollow/Input/HollowCommentInputView.swift b/Source/Hollow/View/Hierarchy/Hollow/Input/HollowCommentInputView.swift index 13081cf2..58716e24 100644 --- a/Source/Hollow/View/Hierarchy/Hollow/Input/HollowCommentInputView.swift +++ b/Source/Hollow/View/Hierarchy/Hollow/Input/HollowCommentInputView.swift @@ -36,7 +36,7 @@ struct HollowCommentInputView: View { var body: some View { VStack(spacing: vstackSpacing) { HStack { - BarButton(action: { withAnimation(transitionAnimation) { store.replyToIndex = -2 }}, systemImageName: "xmark") + BarButton(action: { withAnimation(transitionAnimation) { store.replyToId = -2 }}, systemImageName: "xmark") Spacer() let sendingText = NSLocalizedString("COMMENT_INPUT_SEND_BUTTON_SENDING", comment: "") @@ -110,7 +110,7 @@ struct HollowCommentInputView: View { .onEnded { value in if value.predictedEndTranslation.height > viewSize.height * 2 / 3 { withAnimation(transitionAnimation) { - store.replyToIndex = -2 + store.replyToId = -2 } } } diff --git a/Source/Hollow/View/Hierarchy/Main/MainView.swift b/Source/Hollow/View/Hierarchy/Main/MainView.swift index c7318269..f14c3114 100644 --- a/Source/Hollow/View/Hierarchy/Main/MainView.swift +++ b/Source/Hollow/View/Hierarchy/Main/MainView.swift @@ -75,7 +75,7 @@ struct MainView: View { .overlay(Group { if showCreatePost { - Color.black.opacity(colorScheme == .dark ? 0.2 : 0.1).ignoresSafeArea() + Color.black.opacity(0.2).ignoresSafeArea() } }) .overlay( diff --git a/Source/Hollow/View/Integration/HollowDetailViewController.swift b/Source/Hollow/View/Integration/HollowDetailViewController.swift index c08f1ddf..b43349b6 100644 --- a/Source/Hollow/View/Integration/HollowDetailViewController.swift +++ b/Source/Hollow/View/Integration/HollowDetailViewController.swift @@ -65,13 +65,11 @@ struct HollowDetailViewWrapper: View { fileprivate let wrapper: ViewModelWrapper let isRoot: Bool @State var presented = true - @Environment(\.colorScheme) var colorScheme var body: some View { if isRoot { HollowDetailView(store: wrapper.store) - // Prevent being interrupted by scroll view's gesture - .overlay(Color.black.opacity(0.0001).frame(width: 12).leading()) + .overlay(Color.black.opacity(0.0001).frame(width: 14).leading()) .swipeToDismiss( presented: .init( get: { true }, diff --git a/Source/Hollow/View/Integration/IntegrationUtilities.swift b/Source/Hollow/View/Integration/IntegrationUtilities.swift index e57d5530..2da28765 100644 --- a/Source/Hollow/View/Integration/IntegrationUtilities.swift +++ b/Source/Hollow/View/Integration/IntegrationUtilities.swift @@ -116,7 +116,6 @@ struct IntegrationUtilities { detailVC.view.backgroundColor = nil let navVC = UINavigationController(rootViewController: detailVC) navVC.modalPresentationStyle = .overFullScreen - navVC.setNavigationBarHidden(true, animated: false) navVC.view.backgroundColor = nil topViewController()?.present(navVC, animated: true) } diff --git a/Source/Hollow/View/Utilities/Presentation/Drag/DragEnvironment.swift b/Source/Hollow/View/Utilities/Presentation/Drag/DragEnvironment.swift index a670743b..ac644a9f 100644 --- a/Source/Hollow/View/Utilities/Presentation/Drag/DragEnvironment.swift +++ b/Source/Hollow/View/Utilities/Presentation/Drag/DragEnvironment.swift @@ -68,3 +68,25 @@ fileprivate struct ProposedIgnoringSafeArea: ViewModifier { .ignoresSafeArea(edges: internalIsDragging ? [] : edges) } } + +// MARK: - Padding + +extension View { + func respectivePadding(_ edges: Edge.Set, whenDragging: CGFloat?, whenNot: CGFloat?) -> some View { + self.modifier(RespectivePadding(edges: edges, whenDragging: whenDragging, whenNot: whenNot)) + } +} + +fileprivate struct RespectivePadding: ViewModifier { + let edges: Edge.Set + let whenDragging: CGFloat? + let whenNot: CGFloat? + @State private var internalIsDragging = false + @Environment(\.dragging) var dragging + + func body(content: Content) -> some View { + content + .onChange(of: dragging) { dragging in withAnimation { internalIsDragging = dragging } } + .padding(edges, internalIsDragging ? whenDragging : whenNot) + } +} diff --git a/Source/Hollow/View/Utilities/Presentation/Drag/View+proposedOverlay.swift b/Source/Hollow/View/Utilities/Presentation/Drag/View+proposedOverlay.swift index 1b3a6c49..b5f5cadd 100644 --- a/Source/Hollow/View/Utilities/Presentation/Drag/View+proposedOverlay.swift +++ b/Source/Hollow/View/Utilities/Presentation/Drag/View+proposedOverlay.swift @@ -29,7 +29,7 @@ fileprivate struct ProposedOverlay: ViewModifier { } .overlay(Group { if showOverlay { - Color.black.opacity(colorScheme == .dark ? 0.2 : 0.1).ignoresSafeArea() + Color.black.opacity(0.2).ignoresSafeArea() } }) } diff --git a/Source/Hollow/View/Utilities/Presentation/Drag/View+swipeToDismiss.swift b/Source/Hollow/View/Utilities/Presentation/Drag/View+swipeToDismiss.swift index dd938fed..9d42fcc7 100644 --- a/Source/Hollow/View/Utilities/Presentation/Drag/View+swipeToDismiss.swift +++ b/Source/Hollow/View/Utilities/Presentation/Drag/View+swipeToDismiss.swift @@ -20,7 +20,6 @@ fileprivate struct SwipeToDismiss: ViewModifier { @Binding var presented: Bool @State var offset: (x: CGFloat, y: CGFloat) = (0, 0) @State var scale: CGFloat = 1 - @State var disableScroll = false @GestureState private var isPressed = false let screenWidth = UIScreen.main.bounds.size.width let screenHeight = UIScreen.main.bounds.size.height @@ -47,13 +46,13 @@ fileprivate struct SwipeToDismiss: ViewModifier { // Track gesture completion and failure .updating($isPressed) { value, state, _ in guard !state else { return } - if value.startLocation.x <= 12 && value.location.x - value.startLocation.x > 10 { + if dragValid(with: value) { withAnimation { state = true } } } .onChanged { value in // 12 is less than the standard padding - if value.startLocation.x <= 12 && value.location.x - value.startLocation.x > 10 { + if dragValid(with: value) { withAnimation(offset == (0, 0) ? .defaultSpring : nil) { offset.x = offset(for: value.translation.width) offset.y = offset(for: value.translation.height) @@ -62,7 +61,7 @@ fileprivate struct SwipeToDismiss: ViewModifier { } } .onEnded { value in - guard value.startLocation.x <= 12 && value.location.x - value.startLocation.x > 10 else { return } + guard dragValid(with: value) else { return } if offsetExceeded(with: value) { withAnimation { presented = false @@ -81,15 +80,15 @@ fileprivate struct SwipeToDismiss: ViewModifier { } private func offset(for value: CGFloat) -> CGFloat { - // y = \sqrt{a*(x+a/4)} - a/2, where a > 0 - // y' = 1 && y(0) = 0 + // y = sqrt(a*(x+a/4)) - a/2, where a > 0 + // y'(0) = 1 && y(0) = 0 let a: CGFloat = 200 let offset = sqrt(a * (abs(value) + a / 4)) - a / 2 return value > 0 ? offset : -offset } private func scale(for value: CGFloat) -> CGFloat { - return exp(-0.0003 * value) + return value < 20 ? 1 : exp(-0.0003 * (value - 20)) } private func offsetExceeded(with value: DragGesture.Value) -> Bool { @@ -109,5 +108,7 @@ fileprivate struct SwipeToDismiss: ViewModifier { return false } - + private func dragValid(with value: DragGesture.Value) -> Bool { + return value.startLocation.x <= 20 && (value.translation.width > 10 || abs(value.translation.height) > 10 || offset != (0, 0)) + } } diff --git a/Source/Shared/Model/Net/Contents/AttentionListRequest.swift b/Source/Shared/Model/Net/Contents/AttentionListRequest.swift index ddc4497a..870a5ee6 100644 --- a/Source/Shared/Model/Net/Contents/AttentionListRequest.swift +++ b/Source/Shared/Model/Net/Contents/AttentionListRequest.swift @@ -44,7 +44,7 @@ struct AttentionListRequest: DefaultRequest { var commentData = [CommentData]() if let comments = result.comments, let commentsOfPost = comments[post.pid.string]{ if let comments = commentsOfPost { - commentData = comments.map { $0.toCommentData() } + commentData = comments.toCommentData() } } diff --git a/Source/Shared/Model/Net/Contents/AttentionListSearchRequest.swift b/Source/Shared/Model/Net/Contents/AttentionListSearchRequest.swift index b2ce29a6..0a7c353b 100644 --- a/Source/Shared/Model/Net/Contents/AttentionListSearchRequest.swift +++ b/Source/Shared/Model/Net/Contents/AttentionListSearchRequest.swift @@ -55,7 +55,7 @@ struct AttentionListSearchRequest: DefaultRequest { var commentData = [CommentData]() if let comments = result.comments, let commentsOfPost = comments[post.pid.string]{ if let comments = commentsOfPost { - commentData = comments.map{ $0.toCommentData() } + commentData = comments.toCommentData() } } diff --git a/Source/Shared/Model/Net/Contents/PostDetailRequest.swift b/Source/Shared/Model/Net/Contents/PostDetailRequest.swift index 7ce86b4b..75918093 100644 --- a/Source/Shared/Model/Net/Contents/PostDetailRequest.swift +++ b/Source/Shared/Model/Net/Contents/PostDetailRequest.swift @@ -96,7 +96,7 @@ struct PostDetailRequest: DefaultRequest { } else { // The post has not been cached or there's update. - let comments = result.data?.compactMap({ $0.toCommentData() }) ?? [] + let comments = result.data?.toCommentData() ?? [] let postData = post.toPostData(comments: comments) postWrapper = PostDataWrapper(post: postData) diff --git a/Source/Shared/Model/Net/Contents/PostListRequest.swift b/Source/Shared/Model/Net/Contents/PostListRequest.swift index 6fb3bac0..e9da984d 100644 --- a/Source/Shared/Model/Net/Contents/PostListRequest.swift +++ b/Source/Shared/Model/Net/Contents/PostListRequest.swift @@ -56,9 +56,9 @@ struct PostListRequest: DefaultRequest { // process comments of current post var commentData = [CommentData]() - if let comments = result.comments, let commentsOfPost = comments[post.pid.string]{ + if let comments = result.comments, let commentsOfPost = comments[post.pid.string] { if let comments = commentsOfPost { - commentData = comments.map{ $0.toCommentData() } + commentData = comments.toCommentData() } } diff --git a/Source/Shared/Model/Net/Contents/SearchRequest.swift b/Source/Shared/Model/Net/Contents/SearchRequest.swift index 2d6fdcad..956b2ed7 100644 --- a/Source/Shared/Model/Net/Contents/SearchRequest.swift +++ b/Source/Shared/Model/Net/Contents/SearchRequest.swift @@ -67,9 +67,9 @@ struct SearchRequest: DefaultRequest { // process comments of current post var commentData = [CommentData]() - if let comments = result.comments, let commentsOfPost = comments[post.pid.string]{ + if let commentsOfPost = result.comments?[post.pid.string] { if let comments = commentsOfPost { - commentData = comments.map{ $0.toCommentData() } + commentData = comments.toCommentData() } } diff --git a/Source/Shared/Model/Types/Comment.swift b/Source/Shared/Model/Types/Comment.swift index 0ca3b9e7..c19ea7cb 100644 --- a/Source/Shared/Model/Types/Comment.swift +++ b/Source/Shared/Model/Types/Comment.swift @@ -44,7 +44,7 @@ struct Comment: Codable { extension Comment { /// Convert comment to commentData without image /// - Returns: CommentData - func toCommentData() -> CommentData { + func toCommentData(showAvatar: Bool, showAvatarWhenReversed: Bool) -> CommentData { var image: HollowImage? = nil if let imageMetadata = self.imageMetadata, let imageURL = self.url, let w = imageMetadata.w, let h = imageMetadata.h { @@ -90,11 +90,41 @@ extension Comment { isDz: isDz, replyTo: replyTo, image: image, - hasURL: (text?.links().count ?? 0) > 0, - hasCitedNumbers: (text?.citations().count ?? 0) > 0, + url: text?.links() ?? [], + citedNumbers: text?.citationNumbers() ?? [], + showAvatar: showAvatar, + showAvatarWhenReversed: showAvatarWhenReversed, hash: hash, colorIndex: AvatarGenerator.colorIndex(hash: hash), abbreviation: abbreviation ) } } + +extension Array where Element == Comment { + func toCommentData() -> [CommentData] { + var commentData = [CommentData]() + for index in self.indices { + if index == 0 { + commentData.append( + self[index].toCommentData( + showAvatar: true, + showAvatarWhenReversed: self.count <= 1 ? true : self[index].name != self[index + 1].name + ) + ) + } else if index == self.count - 1 { + commentData.append( + self[index].toCommentData( + showAvatar: self.count <= 1 ? true : self[index].name != self[index - 1].name, + showAvatarWhenReversed: true + ) + ) + } else { + commentData.append(self[index].toCommentData(showAvatar: self[index].name != self[index - 1].name, showAvatarWhenReversed: self[index].name != self[index + 1].name)) + } + + } + + return commentData + } +} diff --git a/Source/Shared/Model/Types/Data/CommentData.swift b/Source/Shared/Model/Types/Data/CommentData.swift index 1e0adbcf..8852de67 100644 --- a/Source/Shared/Model/Types/Data/CommentData.swift +++ b/Source/Shared/Model/Types/Data/CommentData.swift @@ -25,16 +25,13 @@ struct CommentData: Identifiable, Codable { var replyTo: Int var image: HollowImage? - // To avoid scanning the text over and over when the - // text does not has components that needed to be - // rendered as hyperlink, set the variable when initialize - // comment data, and check them to decide whether to call - // the methods to scan the text. - var hasURL = false - var hasCitedNumbers = false - var renderHighlight: Bool { hasURL || hasCitedNumbers } + var url: [String] + var citedNumbers: [Int] + var renderHighlight: Bool { !url.isEmpty || !citedNumbers.isEmpty } // Data used in avatar + var showAvatar: Bool + var showAvatarWhenReversed: Bool var hash: Int var colorIndex: Int var abbreviation: String diff --git a/Source/Shared/Model/Types/Data/PostData.swift b/Source/Shared/Model/Types/Data/PostData.swift index 5363d5c0..484e296f 100644 --- a/Source/Shared/Model/Types/Data/PostData.swift +++ b/Source/Shared/Model/Types/Data/PostData.swift @@ -34,15 +34,10 @@ struct PostData: Identifiable, Codable { // Pre fetched cited post id var citedPostId: Int? - // To avoid scanning the text over and over when the - // text does not has components that needed to be - // rendered as hyperlink, set the variable when initialize - // comment data, and check them to decide whether to call - // the methods to scan the text. - var hasURL = false - var hasCitedNumbers = false - var renderHighlight: Bool { hasURL || hasCitedNumbers } - + var url: [String] + var citedNumbers: [Int] + var renderHighlight: Bool { !url.isEmpty || !citedNumbers.isEmpty } + // Color data used in avatar var hash: Int var colorIndex: Int @@ -84,6 +79,8 @@ extension PostDataWrapper { vote: nil, comments: [], loadingError: nil, + url: [], + citedNumbers: [], hash: hash, colorIndex: colorIndex ) diff --git a/Source/Shared/Model/Types/Post.swift b/Source/Shared/Model/Types/Post.swift index f64c800c..a98a528a 100644 --- a/Source/Shared/Model/Types/Post.swift +++ b/Source/Shared/Model/Types/Post.swift @@ -90,8 +90,8 @@ extension Post { hollowImage: image, vote: vote?.toVoteData(), comments: comments, - citedPostId: text.findCitedPostID(), hasURL: text.links().count > 0, - hasCitedNumbers: text.citations().count > 0, + citedPostId: text.findCitedPostID(), url: text.links(), + citedNumbers: text.citationNumbers(), hash: hash, colorIndex: AvatarGenerator.colorIndex(hash: hash) ) diff --git a/Source/Shared/ViewModel/Hollow/HollowDetailStore.swift b/Source/Shared/ViewModel/Hollow/HollowDetailStore.swift index 4eb42208..fdb821fd 100644 --- a/Source/Shared/ViewModel/Hollow/HollowDetailStore.swift +++ b/Source/Shared/ViewModel/Hollow/HollowDetailStore.swift @@ -23,7 +23,7 @@ class HollowDetailStore: ObservableObject, ImageCompressStore, AppModelEnvironme @Published var noSuchPost = false // MARK: Input Variables - @Published var replyToIndex: Int = -2 + @Published var replyToId: Int = -2 @Published var imageSizeInformation: String? var text: String = "" { didSet { if text == "" || oldValue == "" { @@ -285,9 +285,14 @@ class HollowDetailStore: ObservableObject, ImageCompressStore, AppModelEnvironme func sendComment() { guard let config = Defaults[.hollowConfig], let token = Defaults[.accessToken] else { return } - let replyTo = replyToIndex == -1 ? -1 : postDataWrapper.post.comments[replyToIndex].commentId + let replyTo = replyToId == -1 ? -1 : replyToId var text = self.text - if replyToIndex >= 0 { + if replyTo >= 0 { + guard let replyToIndex = postDataWrapper.post.comments.firstIndex(where: { $0.commentId == replyToId }) else { + withAnimation { self.isSendingComment = false } + self.errorMessage = (title: DefaultRequestError.unknown.description, message: "") + return + } text = "Re \(postDataWrapper.post.comments[replyToIndex].name): " + text } let configuration = SendCommentRequestConfiguration(apiRoot: config.apiRootUrls, token: token, text: text, imageData: compressedImageBase64String, postId: postDataWrapper.post.postId, replyCommentId: replyTo) @@ -310,7 +315,7 @@ class HollowDetailStore: ObservableObject, ImageCompressStore, AppModelEnvironme private func restoreInput() { self.text = "" - self.replyToIndex = -2 + self.replyToId = -2 self.image = nil self.compressedImage = nil self.imageSizeInformation = nil