Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collapse posts on tap (Apollo-style) #2153

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,15 @@ public struct StatusDetailView: View {
.navigationBarTitleDisplayMode(.inline)
}

@ViewBuilder
private func makeStatusesListView(statuses: [Status]) -> some View {
ForEach(statuses) { status in
let collapsedIds = viewModel.hierarchyCollapseState.implicitlyCollapsedStatusIds(for: statuses)
ForEach(statuses.filter { !collapsedIds.contains($0.id) }) { status in
let (indentationLevel, extraInsets) = viewModel.getIndentationLevel(id: status.id, maxIndent: userPreferences.getRealMaxIndent())
let viewModel: StatusRowViewModel = .init(status: status,
client: client,
routerPath: routerPath,
hierarchyCollapseState: viewModel.hierarchyCollapseState,
scrollToId: $viewModel.scrollToId)
let isFocused = self.viewModel.statusId == status.id

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import SwiftUI

var client: Client?
var routerPath: RouterPath?

let hierarchyCollapseState = StatusHierarchyCollapseState()

enum State {
case loading, display(statuses: [Status]), error(error: Error)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Observation
import Models

@MainActor
@Observable public class StatusHierarchyCollapseState {
public var explicitlyCollapsedStatusIds: Set<String>

public init(explicitlyCollapsedStatusIds: Set<String> = []) {
self.explicitlyCollapsedStatusIds = explicitlyCollapsedStatusIds
}

public func implicitlyCollapsedStatusIds(for statuses: [Status]) -> Set<String> {
let childs: [String: [String]] = Dictionary(
grouping: statuses.filter { $0.inReplyToId != nil },
by: { $0.inReplyToId! }
).mapValues { $0.map(\.id) }

func descendants(for id: String) -> [String] {
(childs[id] ?? []).flatMap { [$0] + descendants(for: $0) }
}

return Set(explicitlyCollapsedStatusIds.flatMap(descendants(for:)))
}
}
38 changes: 24 additions & 14 deletions Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,18 @@ public struct StatusRowView: View {
if !isCompact {
StatusRowHeaderView(viewModel: viewModel)
}
StatusRowContentView(viewModel: viewModel)
.contentShape(Rectangle())
.onTapGesture {
guard !isFocused else { return }
viewModel.navigateToDetail()
}
.accessibilityActions {
if isFocused, viewModel.showActions {
accessibilityActions
if !viewModel.isHierarchyExplicitlyCollapsed {
StatusRowContentView(viewModel: viewModel)
.contentShape(Rectangle())
.onTapGesture {
handleTap()
}
}
.accessibilityActions {
if isFocused, viewModel.showActions {
accessibilityActions
}
}
}
if !reasons.contains(.placeholder),
viewModel.showActions, isFocused || theme.statusActionsDisplay != .none,
!isInCaptureMode
Expand Down Expand Up @@ -161,8 +162,7 @@ public struct StatusRowView: View {
? StatusRowAccessibilityLabel(viewModel: viewModel).finalLabel() : Text(""))
.accessibilityHidden(viewModel.filter?.filter.filterAction == .hide)
.accessibilityAction {
guard !isFocused else { return }
viewModel.navigateToDetail()
handleTap()
}
.accessibilityActions {
if !isFocused, viewModel.showActions, accessibilityVoiceOverEnabled {
Expand All @@ -173,8 +173,7 @@ public struct StatusRowView: View {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
guard !isFocused else { return }
viewModel.navigateToDetail()
handleTap()
}
}
.overlay {
Expand Down Expand Up @@ -348,6 +347,17 @@ public struct StatusRowView: View {
.background(Color.black.opacity(0.40))
.transition(.opacity)
}

private func handleTap() {
guard !isFocused else { return }
if indentationLevel > 0, viewModel.hierarchyCollapseState != nil {
withAnimation {
viewModel.isHierarchyExplicitlyCollapsed.toggle()
}
} else {
viewModel.navigateToDetail()
}
}
}

#Preview {
Expand Down
18 changes: 18 additions & 0 deletions Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import SwiftUI

let client: Client
let routerPath: RouterPath

let hierarchyCollapseState: StatusHierarchyCollapseState?

let userFollowedTag: HTMLString.Link?

Expand Down Expand Up @@ -67,6 +69,20 @@ import SwiftUI
}
}
}

// toggled on tap, collapses the post with its hierarchy of replies
var isHierarchyExplicitlyCollapsed: Bool {
get {
hierarchyCollapseState?.explicitlyCollapsedStatusIds.contains(status.id) ?? false
}
set {
if newValue {
hierarchyCollapseState?.explicitlyCollapsedStatusIds.insert(status.id)
} else {
hierarchyCollapseState?.explicitlyCollapsedStatusIds.remove(status.id)
}
}
}

// used by the button to expand a collapsed post
var isCollapsed: Bool = true {
Expand Down Expand Up @@ -162,6 +178,7 @@ import SwiftUI
public init(status: Status,
client: Client,
routerPath: RouterPath,
hierarchyCollapseState: StatusHierarchyCollapseState? = nil,
isRemote: Bool = false,
showActions: Bool = true,
textDisabled: Bool = false,
Expand All @@ -171,6 +188,7 @@ import SwiftUI
finalStatus = status.reblog ?? status
self.client = client
self.routerPath = routerPath
self.hierarchyCollapseState = hierarchyCollapseState
self.isRemote = isRemote
self.showActions = showActions
self.textDisabled = textDisabled
Expand Down
Loading