260 lines
No EOL
11 KiB
Swift
260 lines
No EOL
11 KiB
Swift
import SwiftUI
|
|
|
|
struct UserListView: View {
|
|
@EnvironmentObject var viewModel: PDSViewModel
|
|
@State private var editingUser: PDSUser? = nil
|
|
@State private var newHandle: String = ""
|
|
@State private var showingEditSheet = false
|
|
@State private var showingSuspendAlert = false
|
|
@State private var showingReactivateAlert = false
|
|
@State private var suspensionReason: String = ""
|
|
@State private var selectedUser: PDSUser? = nil
|
|
@State private var isRefreshing = false
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
Group {
|
|
if viewModel.isLoading && viewModel.users.isEmpty {
|
|
ProgressView("Loading users...")
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else if viewModel.users.isEmpty {
|
|
VStack {
|
|
Text("No users found")
|
|
.foregroundColor(.secondary)
|
|
Button("Refresh") {
|
|
Task {
|
|
await viewModel.refreshUsers()
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else {
|
|
List {
|
|
ForEach(viewModel.users) { user in
|
|
UserRow(user: user)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
selectedUser = user
|
|
editingUser = user
|
|
newHandle = user.handle
|
|
showingEditSheet = true
|
|
}
|
|
.contextMenu {
|
|
Button(action: {
|
|
selectedUser = user
|
|
editingUser = user
|
|
newHandle = user.handle
|
|
showingEditSheet = true
|
|
}) {
|
|
Label("Edit Handle", systemImage: "pencil")
|
|
}
|
|
|
|
if user.isActive {
|
|
Button(action: {
|
|
selectedUser = user
|
|
suspensionReason = ""
|
|
showingSuspendAlert = true
|
|
}) {
|
|
Label("Suspend Account", systemImage: "xmark.circle")
|
|
}
|
|
} else {
|
|
Button(action: {
|
|
selectedUser = user
|
|
showingReactivateAlert = true
|
|
}) {
|
|
Label("Reactivate Account", systemImage: "checkmark.circle")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Users")
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
if viewModel.demoMode {
|
|
Text("DEMO MODE")
|
|
.font(.caption)
|
|
.padding(5)
|
|
.background(Color.green.opacity(0.2))
|
|
.foregroundColor(.green)
|
|
.cornerRadius(4)
|
|
}
|
|
}
|
|
}
|
|
.refreshable {
|
|
print("⏳ Pull-to-refresh: Fetching users")
|
|
await viewModel.refreshUsers()
|
|
print("✅ Pull-to-refresh completed")
|
|
}
|
|
.sheet(isPresented: $showingEditSheet) {
|
|
if let user = editingUser {
|
|
NavigationView {
|
|
Form {
|
|
Section(header: Text("Edit User Handle")) {
|
|
TextField("Handle", text: $newHandle)
|
|
|
|
HStack {
|
|
Button("Cancel") {
|
|
showingEditSheet = false
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button("Save") {
|
|
Task {
|
|
if await viewModel.editUserHandle(userId: user.id, newHandle: newHandle) {
|
|
showingEditSheet = false
|
|
}
|
|
}
|
|
}
|
|
.disabled(newHandle.isEmpty || newHandle == user.handle)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Edit \(user.displayName)")
|
|
}
|
|
}
|
|
}
|
|
.alert("Suspend User", isPresented: $showingSuspendAlert) {
|
|
TextField("Reason for suspension", text: $suspensionReason)
|
|
Button("Cancel", role: .cancel) { }
|
|
Button("Suspend", role: .destructive) {
|
|
if let user = selectedUser {
|
|
print("Attempting to suspend user: \(user.id)")
|
|
Task {
|
|
let success = await viewModel.suspendUser(userId: user.id)
|
|
print("Suspend user result: \(success)")
|
|
|
|
// Force UI update
|
|
await MainActor.run {
|
|
viewModel.refreshUI()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} message: {
|
|
if let user = selectedUser {
|
|
Text("Are you sure you want to suspend \(user.displayName)'s account? This will prevent them from using the service.")
|
|
}
|
|
}
|
|
.alert("Reactivate User", isPresented: $showingReactivateAlert) {
|
|
Button("Cancel", role: .cancel) { }
|
|
Button("Reactivate") {
|
|
if let user = selectedUser {
|
|
print("Attempting to reactivate user: \(user.id)")
|
|
Task {
|
|
let success = await viewModel.reactivateUser(userId: user.id)
|
|
print("Reactivate user result: \(success)")
|
|
|
|
// Force UI update
|
|
await MainActor.run {
|
|
viewModel.refreshUI()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} message: {
|
|
if let user = selectedUser {
|
|
Text("Are you sure you want to reactivate \(user.displayName)'s account?")
|
|
}
|
|
}
|
|
}
|
|
.task {
|
|
print("⏳ Task: Fetching users")
|
|
// Always fetch on initial load
|
|
await viewModel.refreshUsers()
|
|
print("✅ Task fetch completed")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct UserRow: View {
|
|
@ObservedObject var user: PDSUser
|
|
|
|
var formattedDate: String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
formatter.timeStyle = .none
|
|
return "Joined: \(formatter.string(from: user.joinedAt))"
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack(spacing: 12) {
|
|
if let avatarURL = user.avatar {
|
|
AsyncImage(url: avatarURL) { image in
|
|
image
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
} placeholder: {
|
|
ProgressView()
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.clipShape(Circle())
|
|
.overlay(
|
|
Circle()
|
|
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
|
|
)
|
|
} else {
|
|
Image(systemName: "person.circle.fill")
|
|
.resizable()
|
|
.foregroundColor(.gray.opacity(0.7))
|
|
.frame(width: 60, height: 60)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
HStack {
|
|
Text(user.displayName)
|
|
.font(.title3)
|
|
.fontWeight(.semibold)
|
|
.lineLimit(1)
|
|
|
|
if !user.isActive {
|
|
Text("SUSPENDED")
|
|
.font(.caption)
|
|
.padding(.horizontal, 5)
|
|
.padding(.vertical, 2)
|
|
.background(Color.red)
|
|
.foregroundColor(.white)
|
|
.cornerRadius(4)
|
|
}
|
|
}
|
|
|
|
Text("@\(user.handle)")
|
|
.font(.subheadline)
|
|
.foregroundColor(.blue)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
|
|
if !user.description.isEmpty {
|
|
Text(user.description)
|
|
.font(.subheadline)
|
|
.foregroundColor(.primary)
|
|
.lineLimit(3)
|
|
.padding(.top, 2)
|
|
}
|
|
|
|
HStack(spacing: 12) {
|
|
Text(formattedDate)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Spacer()
|
|
|
|
Text(user.id)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
.frame(maxWidth: 160)
|
|
.opacity(0.7)
|
|
}
|
|
}
|
|
.padding(.vertical, 10)
|
|
.opacity(user.isActive ? 1.0 : 0.7)
|
|
}
|
|
} |