Improve the display of replies (#1672)

Threads/replies are now shown more clearly. Each reply has an indentation level
(and therefore the number of vertical lines) one more than its direct parent.
This leads to siblings having the same indentation level. It makes
understanding somewhat complex thread structures way easier. Previously, a
reply was only indented if it came directly after its parent. If a toot had
more than one reply, the structure was nearly indecipherable, as it wasn't
clear which the parent post of the second (or later) toot was. An example is
"https://mastodon.social/@mhoye/110452462852364819" and all of its replies.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>
This commit is contained in:
Paul Schuetz 2023-11-16 09:56:00 +01:00 committed by GitHub
parent 4b74532048
commit 59e5eba860
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 31 additions and 31 deletions

View file

@ -25,8 +25,8 @@ private struct IsStatusFocused: EnvironmentKey {
static let defaultValue: Bool = false static let defaultValue: Bool = false
} }
private struct IsStatusReplyToPrevious: EnvironmentKey { private struct IndentationLevel: EnvironmentKey {
static let defaultValue: Bool = false static let defaultValue: UInt = 0
} }
public extension EnvironmentValues { public extension EnvironmentValues {
@ -60,8 +60,8 @@ public extension EnvironmentValues {
set { self[IsStatusFocused.self] = newValue } set { self[IsStatusFocused.self] = newValue }
} }
var isStatusReplyToPrevious: Bool { var indentationLevel: UInt {
get { self[IsStatusReplyToPrevious.self] } get { self[IndentationLevel.self] }
set { self[IsStatusReplyToPrevious.self] = newValue } set { self[IndentationLevel.self] = newValue }
} }
} }

View file

@ -103,15 +103,15 @@ public struct StatusDetailView: View {
private func makeStatusesListView(statuses: [Status]) -> some View { private func makeStatusesListView(statuses: [Status]) -> some View {
ForEach(statuses) { status in ForEach(statuses) { status in
let isReplyToPrevious = viewModel.isReplyToPreviousCache[status.id] ?? false let indentationLevel = viewModel.getIndentationLevel(id: status.id)
let viewModel: StatusRowViewModel = .init(status: status, let viewModel: StatusRowViewModel = .init(status: status,
client: client, client: client,
routerPath: routerPath) routerPath: routerPath)
let isFocused = self.viewModel.statusId == status.id let isFocused = self.viewModel.statusId == status.id
StatusRowView(viewModel: viewModel) StatusRowView(viewModel: viewModel)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0) .environment(\.extraLeadingInset, (indentationLevel > 0) ? 10 : 0)
.environment(\.isStatusReplyToPrevious, isReplyToPrevious) .environment(\.indentationLevel, indentationLevel)
.environment(\.isStatusFocused, isFocused) .environment(\.isStatusFocused, isFocused)
.overlay { .overlay {
if isFocused { if isFocused {

View file

@ -19,9 +19,10 @@ import SwiftUI
var state: State = .loading var state: State = .loading
var title: LocalizedStringKey = "" var title: LocalizedStringKey = ""
var scrollToId: String? var scrollToId: String?
static var maxIndent = UInt(7)
@ObservationIgnored @ObservationIgnored
var isReplyToPreviousCache: [String: Bool] = [:] var indentationLevelPreviousCache: [String: UInt] = [:]
init(statusId: String) { init(statusId: String) {
state = .loading state = .loading
@ -35,7 +36,7 @@ import SwiftUI
statusId = status.id statusId = status.id
remoteStatusURL = nil remoteStatusURL = nil
if status.inReplyToId != nil { if status.inReplyToId != nil {
isReplyToPreviousCache[status.id] = true indentationLevelPreviousCache[status.id] = 1
} }
} }
@ -111,24 +112,15 @@ import SwiftUI
} }
private func cacheReplyTopPrevious(statuses: [Status]) { private func cacheReplyTopPrevious(statuses: [Status]) {
isReplyToPreviousCache = [:] indentationLevelPreviousCache = [:]
for status in statuses { for status in statuses {
var isReplyToPrevious: Bool = false if let inReplyToId = status.inReplyToId,
if let index = statuses.firstIndex(where: { $0.id == status.id }), let prevIndent = indentationLevelPreviousCache[inReplyToId] {
index > 0, indentationLevelPreviousCache[status.id] = prevIndent + 1
statuses[index - 1].id == status.inReplyToId
{
if index == 1, statuses.count > 2 {
let nextStatus = statuses[2]
isReplyToPrevious = nextStatus.inReplyToId == status.id
} else if statuses.count == 2 {
isReplyToPrevious = false
} else { } else {
isReplyToPrevious = true indentationLevelPreviousCache[status.id] = 0
} }
} }
isReplyToPreviousCache[status.id] = isReplyToPrevious
}
} }
func handleEvent(event: any StreamEvent, currentAccount: Account?) { func handleEvent(event: any StreamEvent, currentAccount: Account?) {
@ -146,4 +138,8 @@ import SwiftUI
} }
} }
} }
func getIndentationLevel(id: String) -> UInt {
min(indentationLevelPreviousCache[id] ?? 0, Self.maxIndent)
}
} }

View file

@ -15,7 +15,7 @@ public struct StatusRowView: View {
@Environment(\.isCompact) private var isCompact: Bool @Environment(\.isCompact) private var isCompact: Bool
@Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled @Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled
@Environment(\.isStatusFocused) private var isFocused @Environment(\.isStatusFocused) private var isFocused
@Environment(\.isStatusReplyToPrevious) private var isStatusReplyToPrevious @Environment(\.indentationLevel) private var indentationLevel
@Environment(QuickLook.self) private var quickLook @Environment(QuickLook.self) private var quickLook
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@ -32,11 +32,15 @@ public struct StatusRowView: View {
public var body: some View { public var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
if isStatusReplyToPrevious { HStack(spacing: 3) {
ForEach(0..<indentationLevel, id: \.self) {_ in
Rectangle() Rectangle()
.fill(theme.tintColor) .fill(theme.tintColor)
.frame(width: 2) .frame(width: 2)
.accessibilityHidden(true) .accessibilityHidden(true)
}
}
if indentationLevel > 0 {
Spacer(minLength: 8) Spacer(minLength: 8)
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {