Tighten the inbox, detail, and watch layouts so approval actions feel denser and more direct across compact surfaces.
This commit is contained in:
@@ -31,71 +31,132 @@ struct InboxListView: View {
|
||||
filteredRequests.first?.id
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if filteredRequests.isEmpty {
|
||||
EmptyPaneView(
|
||||
title: "No sign-in requests",
|
||||
message: "New approval requests will appear here as soon as a relying party asks for proof.",
|
||||
systemImage: "tray"
|
||||
)
|
||||
.listRowBackground(Color.clear)
|
||||
} else {
|
||||
ForEach(recentRequests) { request in
|
||||
row(for: request, compact: false)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
private var oldestPendingMinutes: Int? {
|
||||
guard let oldest = model.pendingRequests.min(by: { $0.createdAt < $1.createdAt }) else {
|
||||
return nil
|
||||
}
|
||||
return max(1, Int(Date.now.timeIntervalSince(oldest.createdAt)) / 60)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 10, pinnedViews: []) {
|
||||
InboxHeader(
|
||||
pendingCount: model.pendingRequests.count,
|
||||
oldestMinutes: oldestPendingMinutes
|
||||
)
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.bottom, 6)
|
||||
|
||||
if filteredRequests.isEmpty {
|
||||
EmptyPaneView(
|
||||
title: "No sign-in requests",
|
||||
message: "New approval requests will appear here as soon as a relying party asks for proof.",
|
||||
systemImage: "tray"
|
||||
)
|
||||
.padding(.top, 40)
|
||||
} else {
|
||||
ForEach(recentRequests) { request in
|
||||
row(for: request, compact: false)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
|
||||
if !earlierRequests.isEmpty {
|
||||
Text("Earlier today")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.tracking(0.5)
|
||||
.textCase(.uppercase)
|
||||
.foregroundStyle(Color.idpMutedForeground)
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 4)
|
||||
|
||||
if !earlierRequests.isEmpty {
|
||||
Section {
|
||||
ForEach(earlierRequests) { request in
|
||||
row(for: request, compact: true)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
} header: {
|
||||
Text("Earlier today")
|
||||
.textCase(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 120)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollIndicators(.hidden)
|
||||
.background(Color.idpBackground.ignoresSafeArea())
|
||||
.navigationTitle("Inbox")
|
||||
.animation(.spring(response: 0.35, dampingFraction: 0.88), value: filteredRequests.map(\.id))
|
||||
.idpSearchable(text: $searchText, isPresented: $isSearchPresented)
|
||||
#if !os(macOS)
|
||||
.searchable(text: $searchText, isPresented: $isSearchPresented, placement: .navigationBarDrawer(displayMode: .automatic))
|
||||
#else
|
||||
.searchable(text: $searchText, isPresented: $isSearchPresented)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func row(for request: ApprovalRequest, compact: Bool) -> some View {
|
||||
let handle = model.profile?.handle ?? "@you"
|
||||
let highlighted = highlightedRequestID == request.id
|
||||
let isBusy = model.activeRequestID == request.id
|
||||
let rowContent = ApprovalRow(
|
||||
request: request,
|
||||
handle: handle,
|
||||
compact: compact,
|
||||
highlighted: highlighted,
|
||||
onApprove: compact ? nil : { Task { await model.approve(request) } },
|
||||
onDeny: compact ? nil : {
|
||||
Haptics.warning()
|
||||
Task { await model.reject(request) }
|
||||
},
|
||||
isBusy: isBusy
|
||||
)
|
||||
|
||||
if usesSelection {
|
||||
Button {
|
||||
selectedRequestID = request.id
|
||||
Haptics.selection()
|
||||
} label: {
|
||||
ApprovalRow(
|
||||
request: request,
|
||||
handle: model.profile?.handle ?? "@you",
|
||||
compact: compact,
|
||||
highlighted: highlightedRequestID == request.id
|
||||
)
|
||||
rowContent
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
} else {
|
||||
NavigationLink(value: request.id) {
|
||||
ApprovalRow(
|
||||
request: request,
|
||||
handle: model.profile?.handle ?? "@you",
|
||||
compact: compact,
|
||||
highlighted: highlightedRequestID == request.id
|
||||
)
|
||||
rowContent
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct InboxHeader: View {
|
||||
let pendingCount: Int
|
||||
let oldestMinutes: Int?
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
.fill(Color.idpPrimary)
|
||||
Image(systemName: "shield.lefthalf.filled")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(Color.idpPrimaryForeground)
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
Text("idp.global")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
|
||||
Spacer()
|
||||
|
||||
if pendingCount > 0 {
|
||||
ShadcnBadge(title: "\(pendingCount) pending", tone: .ok)
|
||||
if let oldestMinutes {
|
||||
Text("oldest \(oldestMinutes) min ago")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.idpMutedForeground)
|
||||
}
|
||||
} else {
|
||||
ShadcnBadge(title: "all clear", tone: .ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user