Better status focused screen transition

This commit is contained in:
Thomas Ricouard 2023-09-16 15:04:42 +02:00
parent aaafac8e5a
commit 98035e8530
8 changed files with 56 additions and 24 deletions

View file

@ -21,6 +21,14 @@ private struct IsSupporter: EnvironmentKey {
static let defaultValue: Bool = false static let defaultValue: Bool = false
} }
private struct IsStatusDetailLoaded: EnvironmentKey {
static let defaultValue: Bool = false
}
private struct IsStatusFocused: EnvironmentKey {
static let defaultValue: Bool = false
}
public extension EnvironmentValues { public extension EnvironmentValues {
var isSecondaryColumn: Bool { var isSecondaryColumn: Bool {
get { self[SecondaryColumnKey.self] } get { self[SecondaryColumnKey.self] }
@ -46,4 +54,14 @@ public extension EnvironmentValues {
get { self[IsSupporter.self] } get { self[IsSupporter.self] }
set { self[IsSupporter.self] = newValue } set { self[IsSupporter.self] = newValue }
} }
var isStatusDetailLoaded: Bool {
get { self[IsStatusDetailLoaded.self] }
set { self[IsStatusDetailLoaded.self] = newValue }
}
var isStatusFocused: Bool {
get { self[IsStatusFocused.self] }
set { self[IsStatusFocused.self] = newValue }
}
} }

View file

@ -22,15 +22,15 @@ public struct StatusDetailView: View {
@AccessibilityFocusState private var initialFocusBugWorkaround: Bool @AccessibilityFocusState private var initialFocusBugWorkaround: Bool
public init(statusId: String) { public init(statusId: String) {
_viewModel = StateObject(wrappedValue: .init(statusId: statusId)) _viewModel = StateObject(wrappedValue: { .init(statusId: statusId) }())
} }
public init(status: Status) { public init(status: Status) {
_viewModel = StateObject(wrappedValue: .init(status: status)) _viewModel = StateObject(wrappedValue: { .init(status: status) }())
} }
public init(remoteStatusURL: URL) { public init(remoteStatusURL: URL) {
_viewModel = StateObject(wrappedValue: .init(remoteStatusURL: remoteStatusURL)) _viewModel = StateObject(wrappedValue: { .init(remoteStatusURL: remoteStatusURL) }())
} }
public var body: some View { public var body: some View {
@ -147,8 +147,9 @@ public struct StatusDetailView: View {
private func makeCurrentStatusView(status: Status) -> some View { private func makeCurrentStatusView(status: Status) -> some View {
StatusRowView(viewModel: { .init(status: status, StatusRowView(viewModel: { .init(status: status,
client: client, client: client,
routerPath: routerPath, routerPath: routerPath) })
isFocused: true) }) .environment(\.isStatusFocused, true)
.environment(\.isStatusDetailLoaded, !viewModel.isLoadingContext)
.accessibilityFocused($initialFocusBugWorkaround, equals: true) .accessibilityFocused($initialFocusBugWorkaround, equals: true)
.overlay { .overlay {
GeometryReader { reader in GeometryReader { reader in

View file

@ -12,6 +12,7 @@ public struct StatusRowView: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@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
@EnvironmentObject private var quickLook: QuickLook @EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ -66,20 +67,22 @@ public struct StatusRowView: View {
StatusRowContentView(viewModel: viewModel) StatusRowContentView(viewModel: viewModel)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
guard !isFocused else { return }
viewModel.navigateToDetail() viewModel.navigateToDetail()
} }
.accessibilityActions { .accessibilityActions {
if viewModel.isFocused, viewModel.showActions { if isFocused, viewModel.showActions {
accessibilityActions accessibilityActions
} }
} }
} }
if viewModel.showActions, viewModel.isFocused || theme.statusActionsDisplay != .none, !isInCaptureMode { if viewModel.showActions, isFocused || theme.statusActionsDisplay != .none, !isInCaptureMode {
StatusRowActionsView(viewModel: viewModel) StatusRowActionsView(viewModel: viewModel)
.padding(.top, 8) .padding(.top, 8)
.tint(viewModel.isFocused ? theme.tintColor : .gray) .tint(isFocused ? theme.tintColor : .gray)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
guard !isFocused else { return }
viewModel.navigateToDetail() viewModel.navigateToDetail()
} }
} }
@ -122,15 +125,16 @@ public struct StatusRowView: View {
leading: .layoutPadding, leading: .layoutPadding,
bottom: 12, bottom: 12,
trailing: .layoutPadding)) trailing: .layoutPadding))
.accessibilityElement(children: viewModel.isFocused ? .contain : .combine) .accessibilityElement(children: isFocused ? .contain : .combine)
.accessibilityLabel(viewModel.isFocused == false && accessibilityVoiceOverEnabled .accessibilityLabel(isFocused == false && accessibilityVoiceOverEnabled
? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text("")) ? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text(""))
.accessibilityHidden(viewModel.filter?.filter.filterAction == .hide) .accessibilityHidden(viewModel.filter?.filter.filterAction == .hide)
.accessibilityAction { .accessibilityAction {
guard !isFocused else { return }
viewModel.navigateToDetail() viewModel.navigateToDetail()
} }
.accessibilityActions { .accessibilityActions {
if viewModel.isFocused == false, viewModel.showActions { if isFocused == false, viewModel.showActions {
accessibilityActions accessibilityActions
} }
} }
@ -138,6 +142,7 @@ public struct StatusRowView: View {
Color.clear Color.clear
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
guard !isFocused else { return }
viewModel.navigateToDetail() viewModel.navigateToDetail()
} }
} }

View file

@ -9,7 +9,6 @@ import SwiftUI
@MainActor @MainActor
public class StatusRowViewModel: ObservableObject { public class StatusRowViewModel: ObservableObject {
let status: Status let status: Status
let isFocused: Bool
// Whether this status is on a remote local timeline (many actions are unavailable if so) // Whether this status is on a remote local timeline (many actions are unavailable if so)
let isRemote: Bool let isRemote: Bool
let showActions: Bool let showActions: Bool
@ -102,7 +101,6 @@ public class StatusRowViewModel: ObservableObject {
public init(status: Status, public init(status: Status,
client: Client, client: Client,
routerPath: RouterPath, routerPath: RouterPath,
isFocused: Bool = false,
isRemote: Bool = false, isRemote: Bool = false,
showActions: Bool = true, showActions: Bool = true,
textDisabled: Bool = false) textDisabled: Bool = false)
@ -111,7 +109,6 @@ public class StatusRowViewModel: ObservableObject {
finalStatus = status.reblog ?? status finalStatus = status.reblog ?? status
self.client = client self.client = client
self.routerPath = routerPath self.routerPath = routerPath
self.isFocused = isFocused
self.isRemote = isRemote self.isRemote = isRemote
self.showActions = showActions self.showActions = showActions
self.textDisabled = textDisabled self.textDisabled = textDisabled
@ -159,7 +156,6 @@ public class StatusRowViewModel: ObservableObject {
} }
func navigateToDetail() { func navigateToDetail() {
guard !isFocused else { return }
if isRemote, let url = URL(string: finalStatus.url ?? "") { if isRemote, let url = URL(string: finalStatus.url ?? "") {
routerPath.navigate(to: .remoteStatusDetail(url: url)) routerPath.navigate(to: .remoteStatusDetail(url: url))
} else { } else {

View file

@ -9,7 +9,12 @@ struct StatusRowActionsView: View {
@EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var statusDataController: StatusDataController @EnvironmentObject private var statusDataController: StatusDataController
@EnvironmentObject private var userPreferences: UserPreferences @EnvironmentObject private var userPreferences: UserPreferences
@Environment(\.isStatusFocused) private var isFocused
@Environment(\.isStatusDetailLoaded) private var isStatusDetailLoaded
@ObservedObject var viewModel: StatusRowViewModel @ObservedObject var viewModel: StatusRowViewModel
func privateBoost() -> Bool { func privateBoost() -> Bool {
viewModel.status.visibility == .priv && viewModel.status.account.id == currentAccount.account?.id viewModel.status.visibility == .priv && viewModel.status.account.id == currentAccount.account?.id
@ -75,8 +80,8 @@ struct StatusRowActionsView: View {
} }
} }
func count(dataController: StatusDataController, viewModel: StatusRowViewModel, theme: Theme) -> Int? { func count(dataController: StatusDataController, isFocused: Bool, theme: Theme) -> Int? {
if theme.statusActionsDisplay == .discret, !viewModel.isFocused { if theme.statusActionsDisplay == .discret, isFocused {
return nil return nil
} }
switch self { switch self {
@ -148,8 +153,11 @@ struct StatusRowActionsView: View {
} }
} }
} }
if viewModel.isFocused {
if isStatusDetailLoaded {
StatusRowDetailView(viewModel: viewModel) StatusRowDetailView(viewModel: viewModel)
.transition(.move(edge: .bottom))
.animation(.snappy)
} }
} }
} }
@ -179,7 +187,7 @@ struct StatusRowActionsView: View {
.disabled(action == .boost && .disabled(action == .boost &&
(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id)) (viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id))
if let count = action.count(dataController: statusDataController, if let count = action.count(dataController: statusDataController,
viewModel: viewModel, isFocused: isFocused,
theme: theme), !viewModel.isRemote theme: theme), !viewModel.isRemote
{ {
Text("\(count)") Text("\(count)")

View file

@ -6,6 +6,7 @@ import SwiftUI
struct StatusRowContentView: View { struct StatusRowContentView: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@Environment(\.isCompact) private var isCompact @Environment(\.isCompact) private var isCompact
@Environment(\.isStatusFocused) private var isFocused
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ -44,7 +45,7 @@ struct StatusRowContentView: View {
Spacer() Spacer()
} }
} }
.accessibilityHidden(viewModel.isFocused == false) .accessibilityHidden(isFocused == false)
.padding(.vertical, 4) .padding(.vertical, 4)
} }

View file

@ -5,6 +5,8 @@ import SwiftUI
struct StatusRowHeaderView: View { struct StatusRowHeaderView: View {
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool @Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
@Environment(\.isStatusFocused) private var isFocused
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
let viewModel: StatusRowViewModel let viewModel: StatusRowViewModel
@ -29,7 +31,7 @@ struct StatusRowHeaderView: View {
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account) viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
} }
.accessibilityActions { .accessibilityActions {
if viewModel.isFocused { if isFocused {
StatusRowContextMenu(viewModel: viewModel) StatusRowContextMenu(viewModel: viewModel)
} }
} }

View file

@ -5,6 +5,7 @@ import SwiftUI
struct StatusRowTextView: View { struct StatusRowTextView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@Environment(\.isStatusFocused) private var isFocused
@ObservedObject var viewModel: StatusRowViewModel @ObservedObject var viewModel: StatusRowViewModel
@ -15,11 +16,11 @@ struct StatusRowTextView: View {
emojis: viewModel.finalStatus.emojis, emojis: viewModel.finalStatus.emojis,
language: viewModel.finalStatus.language, language: viewModel.finalStatus.language,
lineLimit: viewModel.lineLimit) lineLimit: viewModel.lineLimit)
.font(viewModel.isFocused ? .scaledBodyFocused : .scaledBody) .font(isFocused ? .scaledBodyFocused : .scaledBody)
.lineSpacing(CGFloat(theme.lineSpacing)) .lineSpacing(CGFloat(theme.lineSpacing))
.foregroundColor(viewModel.textDisabled ? .gray : theme.labelColor) .foregroundColor(viewModel.textDisabled ? .gray : theme.labelColor)
.emojiSize(viewModel.isFocused ? Font.scaledBodyFocusedFont.emojiSize : Font.scaledBodyFont.emojiSize) .emojiSize(isFocused ? Font.scaledBodyFocusedFont.emojiSize : Font.scaledBodyFont.emojiSize)
.emojiBaselineOffset(viewModel.isFocused ? Font.scaledBodyFocusedFont.emojiBaselineOffset : Font.scaledBodyFont.emojiBaselineOffset) .emojiBaselineOffset(isFocused ? Font.scaledBodyFocusedFont.emojiBaselineOffset : Font.scaledBodyFont.emojiBaselineOffset)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
viewModel.routerPath.handleStatus(status: viewModel.finalStatus, url: url) viewModel.routerPath.handleStatus(status: viewModel.finalStatus, url: url)
}) })