diff --git a/PDSMan/Models/PDSModels.swift b/PDSMan/Models/PDSModels.swift index 1bd895c..9316460 100644 --- a/PDSMan/Models/PDSModels.swift +++ b/PDSMan/Models/PDSModels.swift @@ -36,14 +36,25 @@ struct CodeUse: Codable, Identifiable, Sendable { } // User model -struct PDSUser: Identifiable, Hashable, Sendable { - var id: String // DID - var handle: String - var displayName: String - var description: String - var joinedAt: Date - var avatar: URL? - var isActive: Bool = true +class PDSUser: Identifiable, Hashable, Sendable, ObservableObject { + let id: String // DID + let joinedAt: Date + let avatar: URL? + + @Published var handle: String + @Published var displayName: String + @Published var description: String + @Published var isActive: Bool = true + + init(id: String, handle: String, displayName: String, description: String, joinedAt: Date, avatar: URL?, isActive: Bool = true) { + self.id = id + self.handle = handle + self.displayName = displayName + self.description = description + self.joinedAt = joinedAt + self.avatar = avatar + self.isActive = isActive + } // Add Hashable conformance func hash(into hasher: inout Hasher) { diff --git a/PDSMan/ViewModels/PDSViewModel.swift b/PDSMan/ViewModels/PDSViewModel.swift index e7fe7a6..9de656d 100644 --- a/PDSMan/ViewModels/PDSViewModel.swift +++ b/PDSMan/ViewModels/PDSViewModel.swift @@ -6,6 +6,11 @@ import Combine class PDSViewModel: ObservableObject { @Published var pdsService = PDSService() @Published var alertItem: AlertItem? + @Published var demoMode: Bool = false + + // Mock data for demo mode + private var mockUsers: [PDSUser] = [] + private var mockInviteCodes: [InviteCode] = [] var errorMessage: String? { pdsService.errorMessage @@ -17,6 +22,9 @@ class PDSViewModel: ObservableObject { var isAuthenticated: Bool { get { + if demoMode { + return true // Always authenticated in demo mode + } let value = pdsService.isAuthenticated print("PDSViewModel: isAuthenticated getter called, value = \(value)") return value @@ -24,11 +32,17 @@ class PDSViewModel: ObservableObject { } var users: [PDSUser] { - pdsService.users + if demoMode { + return mockUsers + } + return pdsService.users } var inviteCodes: [InviteCode] { - pdsService.inviteCodes + if demoMode { + return mockInviteCodes + } + return pdsService.inviteCodes } // Add listeners for PDSService changes @@ -48,30 +62,214 @@ class PDSViewModel: ObservableObject { // Method to manually refresh UI data func refreshUI() { + print("PDSViewModel: refreshUI called") DispatchQueue.main.async { + print("PDSViewModel: sending objectWillChange notification") self.objectWillChange.send() + + if self.demoMode { + print("Demo users status: \(self.mockUsers.map { "\($0.handle): \($0.isActive)" }.joined(separator: ", "))") + } } } + // Enable demo mode with mock data + func enableDemoMode() { + demoMode = true + createMockData() + refreshUI() + } + + // Disable demo mode + func disableDemoMode() { + demoMode = false + refreshUI() + } + + // Create mock data for demo mode + private func createMockData() { + // Create mock users + mockUsers = [ + PDSUser( + id: "did:plc:abcdef123456", + handle: "alice", + displayName: "Alice Smith", + description: "Product designer and developer. Working on decentralized social apps.", + joinedAt: Date().addingTimeInterval(-86400 * 30), // 30 days ago + avatar: nil, + isActive: true + ), + PDSUser( + id: "did:plc:ghijkl789012", + handle: "bob", + displayName: "Bob Johnson", + description: "Software engineer. Interested in decentralized protocols and privacy.", + joinedAt: Date().addingTimeInterval(-86400 * 15), // 15 days ago + avatar: nil, + isActive: true + ), + PDSUser( + id: "did:plc:mnopqr345678", + handle: "charlie", + displayName: "Charlie Brown", + description: "Community manager and content creator.", + joinedAt: Date().addingTimeInterval(-86400 * 5), // 5 days ago + avatar: nil, + isActive: false + ) + ] + + // Create mock invite codes + mockInviteCodes = [ + InviteCode( + id: "abc-123-xyz", + uses: [], + createdAt: Date().addingTimeInterval(-86400 * 20), // 20 days ago + disabled: false + ), + InviteCode( + id: "def-456-uvw", + uses: [ + CodeUse(usedBy: "did:plc:abcdef123456", usedAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400 * 10))) + ], + createdAt: Date().addingTimeInterval(-86400 * 25), // 25 days ago + disabled: false + ), + InviteCode( + id: "ghi-789-rst", + uses: [], + createdAt: Date().addingTimeInterval(-86400 * 15), // 15 days ago + disabled: true + ) + ] + } + // Refresh invite codes with UI update func refreshInviteCodes() async { + if demoMode { + // No need to do anything in demo mode, data is already loaded + refreshUI() + return + } + await pdsService.fetchInviteCodes() refreshUI() } // Refresh users with UI update func refreshUsers() async { + if demoMode { + // No need to do anything in demo mode, data is already loaded + refreshUI() + return + } + await pdsService.fetchUsers() refreshUI() } // Disable invite code with guaranteed UI update func disableInviteCode(_ code: String) async -> Bool { + if demoMode { + // In demo mode, just update our mock data + if let index = mockInviteCodes.firstIndex(where: { $0.id == code }) { + mockInviteCodes[index].disabled = true + refreshUI() + return true + } + return false + } + let result = await pdsService.disableInviteCode(code) refreshUI() return result } + // Create invite code in demo mode + func createInviteCode() async -> InviteCode? { + if demoMode { + // Generate a random code in demo mode + let randomCode = "demo-\(Int.random(in: 100...999))-\(UUID().uuidString.prefix(4))" + let newCode = InviteCode( + id: randomCode, + uses: [], + createdAt: Date(), + disabled: false + ) + mockInviteCodes.append(newCode) + refreshUI() + return newCode + } + + let code = await pdsService.createInviteCode() + await refreshInviteCodes() + return code + } + + // Suspend user in demo mode + func suspendUser(userId: String) async -> Bool { + if demoMode { + print("Suspending user in demo mode: \(userId)") + return await withCheckedContinuation { continuation in + DispatchQueue.main.async { + if let user = self.mockUsers.first(where: { $0.id == userId }) { + print("Found user \(user.displayName), setting isActive to false") + user.isActive = false + self.refreshUI() + continuation.resume(returning: true) + } else { + print("User not found in mockUsers: \(userId)") + continuation.resume(returning: false) + } + } + } + } + + let success = await pdsService.suspendUser(userId: userId, reason: "Demo suspension") + await refreshUsers() + return success + } + + // Reactivate user in demo mode + func reactivateUser(userId: String) async -> Bool { + if demoMode { + print("Reactivating user in demo mode: \(userId)") + return await withCheckedContinuation { continuation in + DispatchQueue.main.async { + if let user = self.mockUsers.first(where: { $0.id == userId }) { + print("Found user \(user.displayName), setting isActive to true") + user.isActive = true + self.refreshUI() + continuation.resume(returning: true) + } else { + print("User not found in mockUsers: \(userId)") + continuation.resume(returning: false) + } + } + } + } + + let success = await pdsService.reactivateUser(userId: userId) + await refreshUsers() + return success + } + + // Edit user handle in demo mode + func editUserHandle(userId: String, newHandle: String) async -> Bool { + if demoMode { + if let user = mockUsers.first(where: { $0.id == userId }) { + user.handle = newHandle + refreshUI() + return true + } + return false + } + + let success = await pdsService.editUserHandle(userId: userId, newHandle: newHandle) + await refreshUsers() + return success + } + func login(serverURL: String, username: String, password: String) async { print("PDSViewModel: login called") if let credentials = PDSCredentials(serverURL: serverURL, username: username, password: password) { @@ -87,7 +285,11 @@ class PDSViewModel: ObservableObject { func logout() { print("PDSViewModel: logout called") - pdsService.logout() + if demoMode { + disableDemoMode() + } else { + pdsService.logout() + } objectWillChange.send() } } diff --git a/PDSMan/Views/InviteCodesView.swift b/PDSMan/Views/InviteCodesView.swift index df599ba..bcaf1ea 100644 --- a/PDSMan/Views/InviteCodesView.swift +++ b/PDSMan/Views/InviteCodesView.swift @@ -83,12 +83,22 @@ struct InviteCodesView: View { } .navigationTitle("Invite Codes") .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + if viewModel.demoMode { + Text("DEMO MODE") + .font(.caption) + .padding(5) + .background(Color.green.opacity(0.2)) + .foregroundColor(.green) + .cornerRadius(4) + } + } + ToolbarItem(placement: .navigationBarTrailing) { Button { isCreatingCode = true Task { - await viewModel.pdsService.createInviteCode() - await viewModel.refreshInviteCodes() + await viewModel.createInviteCode() isCreatingCode = false } } label: { diff --git a/PDSMan/Views/LoginView.swift b/PDSMan/Views/LoginView.swift index 40ebf46..cd0fa48 100644 --- a/PDSMan/Views/LoginView.swift +++ b/PDSMan/Views/LoginView.swift @@ -64,11 +64,32 @@ struct LoginView: View { .cornerRadius(8) } - Button("Toggle Debug Info") { + Divider() + .padding(.vertical, 10) + .padding(.horizontal, 40) + + Button { + enterDemoMode() + } label: { + HStack { + Image(systemName: "rectangle.fill.on.rectangle.fill.circle") + .imageScale(.medium) + Text("Enter Demo Mode") + } + .frame(maxWidth: .infinity) + .padding() + } + .background(Color.green) + .foregroundColor(.white) + .cornerRadius(10) + .padding(.horizontal, 40) + + Button("Show Debug Info") { showingDebugInfo.toggle() } .font(.caption) - .padding(.top, 20) + .foregroundColor(.secondary) + .padding(.top, 10) if showingDebugInfo { ScrollView { @@ -99,6 +120,11 @@ struct LoginView: View { } } + private func enterDemoMode() { + debugLogs.append("Entering demo mode...") + viewModel.enableDemoMode() + } + private func login() { isLoggingIn = true diff --git a/PDSMan/Views/UserListView.swift b/PDSMan/Views/UserListView.swift index 217dff4..fd3d336 100644 --- a/PDSMan/Views/UserListView.swift +++ b/PDSMan/Views/UserListView.swift @@ -72,6 +72,18 @@ struct UserListView: View { } } .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() @@ -93,9 +105,8 @@ struct UserListView: View { Button("Save") { Task { - if await viewModel.pdsService.editUserHandle(userId: user.id, newHandle: newHandle) { + if await viewModel.editUserHandle(userId: user.id, newHandle: newHandle) { showingEditSheet = false - await viewModel.refreshUsers() // Refresh to show updated handle } } } @@ -112,9 +123,15 @@ struct UserListView: View { Button("Cancel", role: .cancel) { } Button("Suspend", role: .destructive) { if let user = selectedUser { + print("Attempting to suspend user: \(user.id)") Task { - await viewModel.pdsService.suspendUser(userId: user.id, reason: suspensionReason) - await viewModel.refreshUsers() // Refresh after suspension + let success = await viewModel.suspendUser(userId: user.id) + print("Suspend user result: \(success)") + + // Force UI update + await MainActor.run { + viewModel.refreshUI() + } } } } @@ -127,9 +144,15 @@ struct UserListView: View { Button("Cancel", role: .cancel) { } Button("Reactivate") { if let user = selectedUser { + print("Attempting to reactivate user: \(user.id)") Task { - await viewModel.pdsService.reactivateUser(userId: user.id) - await viewModel.refreshUsers() // Refresh after reactivation + let success = await viewModel.reactivateUser(userId: user.id) + print("Reactivate user result: \(success)") + + // Force UI update + await MainActor.run { + viewModel.refreshUI() + } } } } @@ -149,7 +172,7 @@ struct UserListView: View { } struct UserRow: View { - let user: PDSUser + @ObservedObject var user: PDSUser var formattedDate: String { let formatter = DateFormatter()