From bbad8d6948165742e3376cd5f15c0b8cf29f1fef Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Wed, 19 Mar 2025 01:55:58 -0600 Subject: [PATCH] Bug fixes --- PDSMan.xcodeproj/project.pbxproj | 4 +- PDSMan/Services/PDSService.swift | 216 +++++++++++++++------------ PDSMan/ViewModels/PDSViewModel.swift | 41 +++++ PDSMan/Views/InviteCodesView.swift | 51 +++---- PDSMan/Views/UserListView.swift | 49 ++---- 5 files changed, 195 insertions(+), 166 deletions(-) diff --git a/PDSMan.xcodeproj/project.pbxproj b/PDSMan.xcodeproj/project.pbxproj index 4ddab58..72ae098 100644 --- a/PDSMan.xcodeproj/project.pbxproj +++ b/PDSMan.xcodeproj/project.pbxproj @@ -395,7 +395,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"PDSMan/Preview Content\""; DEVELOPMENT_TEAM = 4BC9Y2LL4B; ENABLE_PREVIEWS = YES; @@ -431,7 +431,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"PDSMan/Preview Content\""; DEVELOPMENT_TEAM = 4BC9Y2LL4B; ENABLE_PREVIEWS = YES; diff --git a/PDSMan/Services/PDSService.swift b/PDSMan/Services/PDSService.swift index 2f1ecf7..402aeb2 100644 --- a/PDSMan/Services/PDSService.swift +++ b/PDSMan/Services/PDSService.swift @@ -387,76 +387,84 @@ class PDSService: ObservableObject { // MARK: - Invite Codes func fetchInviteCodes() async { - guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return } - + print("⏳ PDSService: Starting to fetch invite codes") self.isLoading = true + defer { self.isLoading = false } - defer { - self.isLoading = false + guard let baseURL = baseURL, let authHeader = authHeader else { + print("❌ PDSService: Cannot fetch invite codes - missing authentication") + return } - // Construct the URL for the invite codes endpoint - guard let inviteCodesURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.getInviteCodes") else { - setError("Invalid invite codes URL") + // Set up URL components for the request with any needed query parameters + guard var components = URLComponents(string: "\(baseURL)/xrpc/com.atproto.admin.getInviteCodes") else { + print("❌ PDSService: Invalid invite codes URL") return } // Add query parameters - var components = URLComponents(url: inviteCodesURL, resolvingAgainstBaseURL: true) - components?.queryItems = [ + components.queryItems = [ URLQueryItem(name: "sort", value: "recent"), URLQueryItem(name: "limit", value: "100"), - URLQueryItem(name: "includeDisabled", value: "true") // Always include disabled codes + URLQueryItem(name: "includeDisabled", value: "true") ] - guard let finalURL = components?.url else { - setError("Invalid invite codes URL with parameters") + guard let url = components.url else { + print("❌ PDSService: Failed to construct URL with query parameters") return } - var request = URLRequest(url: finalURL) + var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue(authHeader, forHTTPHeaderField: "Authorization") do { let (data, response) = try await session.data(for: request) - guard let httpResponse = response as? HTTPURLResponse else { - setError("Invalid response from server") + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 + print("❌ PDSService: Invite codes fetch failed with status \(statusCode)") return } - if httpResponse.statusCode == 200 { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - - let codesResponse = try decoder.decode(InviteCodesResponse.self, from: data) - + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + // Debug: Print raw response + if let responseString = String(data: data, encoding: .utf8) { + print("👀 PDSService: Raw invite codes response: \(responseString)") + } + + let codesResponse = try decoder.decode(InviteCodesResponse.self, from: data) + + // Map the response to our model + let parsedCodes = codesResponse.codes.map { codeResp -> InviteCode in let dateFormatter = ISO8601DateFormatter() + let createdDate = dateFormatter.date(from: codeResp.createdAt) ?? Date() - let parsedCodes = codesResponse.codes.map { codeResp -> InviteCode in - let createdDate = dateFormatter.date(from: codeResp.createdAt) ?? Date() - - // Convert the uses array - let inviteUses = codeResp.uses?.map { use -> PDSMan.CodeUse in - return PDSMan.CodeUse(usedBy: use.usedBy, usedAt: use.usedAt) - } - - return InviteCode( - id: codeResp.code, - uses: inviteUses, - createdAt: createdDate, - disabled: codeResp.disabled - ) + // Convert the uses array + let inviteUses = codeResp.uses?.map { use -> PDSMan.CodeUse in + return PDSMan.CodeUse(usedBy: use.usedBy, usedAt: use.usedAt) } - self.inviteCodes = parsedCodes - } else { - let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" - setError("Failed to fetch invite codes: \(httpResponse.statusCode) - \(responseString)") + return InviteCode( + id: codeResp.code, + uses: inviteUses, + createdAt: createdDate, + disabled: codeResp.disabled + ) } + + // Update the inviteCodes property + DispatchQueue.main.async { + self.inviteCodes = parsedCodes + self.objectWillChange.send() + print("✅ PDSService: Successfully fetched \(parsedCodes.count) invite codes") + print("✅ PDSService: Including \(parsedCodes.filter { !$0.disabled }.count) active codes") + } + } catch { - setError("Failed to fetch invite codes: \(error.localizedDescription)") + print("❌ PDSService: Error fetching invite codes: \(error.localizedDescription)") } } @@ -501,7 +509,13 @@ class PDSService: ObservableObject { ) // Update the local list - self.inviteCodes.append(newCode) + DispatchQueue.main.async { + self.inviteCodes.append(newCode) + self.objectWillChange.send() + } + + // Also refresh the full list to ensure we have the most up-to-date data + await fetchInviteCodes() return newCode } else { @@ -516,10 +530,17 @@ class PDSService: ObservableObject { } func disableInviteCode(_ code: String) async -> Bool { - guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return false } + print("⏳ PDSService: Attempting to disable invite code: \(code)") + self.isLoading = true + defer { self.isLoading = false } + + guard let baseURL = baseURL, let authHeader = authHeader else { + print("❌ PDSService: Cannot disable code - missing authentication") + return false + } guard let disableURL = URL(string: "\(baseURL)/xrpc/com.atproto.admin.disableInviteCodes") else { - setError("Invalid disable invite code URL") + print("❌ PDSService: Invalid disable code URL") return false } @@ -528,7 +549,7 @@ class PDSService: ObservableObject { request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(authHeader, forHTTPHeaderField: "Authorization") - // Create the request body with an array of codes + // Create the request body let disableBody = ["codes": [code]] do { @@ -538,21 +559,29 @@ class PDSService: ObservableObject { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { - setError("Invalid response from server") + print("❌ PDSService: Invalid response from server") return false } + // Debug: Print response details + if let responseString = String(data: data, encoding: .utf8) { + print("👀 PDSService: Disable code response: \(responseString)") + } + if httpResponse.statusCode == 200 { - // Refresh the invite codes + print("✅ PDSService: Successfully disabled code: \(code)") + + // Refresh the invite codes list await fetchInviteCodes() + return true } else { let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" - setError("Failed to disable invite code: \(httpResponse.statusCode) - \(responseString)") + print("❌ PDSService: Failed to disable code: \(httpResponse.statusCode) - \(responseString)") return false } } catch { - setError("Failed to disable invite code: \(error.localizedDescription)") + print("❌ PDSService: Error disabling code: \(error.localizedDescription)") return false } } @@ -560,68 +589,67 @@ class PDSService: ObservableObject { // MARK: - Users func fetchUsers() async { - guard isAuthenticated, let baseURL = baseURL, let authHeader = authHeader else { return } - + print("⏳ PDSService: Starting to fetch users") self.isLoading = true + defer { self.isLoading = false } - defer { - self.isLoading = false - } - - // Construct the URL for the repos endpoint - guard let reposURL = URL(string: "\(baseURL)/xrpc/com.atproto.sync.listRepos") else { - setError("Invalid list repos URL") + guard let baseURL = baseURL, let authHeader = authHeader else { + print("❌ PDSService: Cannot fetch users - missing authentication") return } - // Add query parameters - var components = URLComponents(url: reposURL, resolvingAgainstBaseURL: true) - components?.queryItems = [ - URLQueryItem(name: "limit", value: "100") - ] - - guard let finalURL = components?.url else { - setError("Invalid repos URL with parameters") + // First, get a list of all repos (users) on the server + guard let repoURL = URL(string: "\(baseURL)/xrpc/com.atproto.sync.listRepos") else { + print("❌ PDSService: Invalid list repos URL") return } - var request = URLRequest(url: finalURL) - request.httpMethod = "GET" - request.addValue(authHeader, forHTTPHeaderField: "Authorization") + var repoRequest = URLRequest(url: repoURL) + repoRequest.httpMethod = "GET" + repoRequest.addValue(authHeader, forHTTPHeaderField: "Authorization") do { - let (data, response) = try await session.data(for: request) + let (repoData, repoResponse) = try await session.data(for: repoRequest) - guard let httpResponse = response as? HTTPURLResponse else { - setError("Invalid response from server") + guard let httpResponse = repoResponse as? HTTPURLResponse, httpResponse.statusCode == 200 else { + let statusCode = (repoResponse as? HTTPURLResponse)?.statusCode ?? 0 + print("❌ PDSService: Repos fetch failed with status \(statusCode)") return } - if httpResponse.statusCode == 200 { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - - let reposResponse = try decoder.decode(RepoResponse.self, from: data) - - // Fetch details for each user - var fetchedUsers: [PDSUser] = [] - - for repo in reposResponse.repos { - if let user = await fetchUserProfile(did: repo.did, isActive: repo.active) { - fetchedUsers.append(user) - } - } - - // Sort users by join date (newest first) - fetchedUsers.sort { $0.joinedAt > $1.joinedAt } - - self.users = fetchedUsers - } else { - let responseString = String(data: data, encoding: .utf8) ?? "Unknown error" - setError("Failed to fetch users: \(httpResponse.statusCode) - \(responseString)") + // Debug: Print raw response + if let responseString = String(data: repoData, encoding: .utf8) { + print("👀 PDSService: Raw repos response: \(responseString)") } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let reposResult = try decoder.decode(RepoResponse.self, from: repoData) + print("📊 PDSService: Found \(reposResult.repos.count) repos") + + // Fetch individual user profiles + var fetchedUsers: [PDSUser] = [] + + for repo in reposResult.repos { + print("🔍 PDSService: Fetching profile for \(repo.did)") + if let user = await fetchUserProfile(did: repo.did, isActive: repo.active) { + fetchedUsers.append(user) + } + } + + // Sort users by join date (newest first) + fetchedUsers.sort { $0.joinedAt > $1.joinedAt } + + // Update the users property + DispatchQueue.main.async { + self.users = fetchedUsers + self.objectWillChange.send() + print("✅ PDSService: Successfully fetched \(fetchedUsers.count) user profiles") + } + } catch { - setError("Failed to fetch users: \(error.localizedDescription)") + print("❌ PDSService: Error fetching repos: \(error.localizedDescription)") } } diff --git a/PDSMan/ViewModels/PDSViewModel.swift b/PDSMan/ViewModels/PDSViewModel.swift index 213371c..e7fe7a6 100644 --- a/PDSMan/ViewModels/PDSViewModel.swift +++ b/PDSMan/ViewModels/PDSViewModel.swift @@ -31,6 +31,47 @@ class PDSViewModel: ObservableObject { pdsService.inviteCodes } + // Add listeners for PDSService changes + init() { + // Subscribe to PDSService objectWillChange events + pdsService.objectWillChange.sink { [weak self] _ in + // Forward the change notification to our own objectWillChange + DispatchQueue.main.async { + self?.objectWillChange.send() + } + } + .store(in: &cancellables) + } + + // Storage for cancellables + private var cancellables = Set() + + // Method to manually refresh UI data + func refreshUI() { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + + // Refresh invite codes with UI update + func refreshInviteCodes() async { + await pdsService.fetchInviteCodes() + refreshUI() + } + + // Refresh users with UI update + func refreshUsers() async { + await pdsService.fetchUsers() + refreshUI() + } + + // Disable invite code with guaranteed UI update + func disableInviteCode(_ code: String) async -> Bool { + let result = await pdsService.disableInviteCode(code) + refreshUI() + return result + } + func login(serverURL: String, username: String, password: String) async { print("PDSViewModel: login called") if let credentials = PDSCredentials(serverURL: serverURL, username: username, password: password) { diff --git a/PDSMan/Views/InviteCodesView.swift b/PDSMan/Views/InviteCodesView.swift index 6c8993b..df599ba 100644 --- a/PDSMan/Views/InviteCodesView.swift +++ b/PDSMan/Views/InviteCodesView.swift @@ -34,7 +34,9 @@ struct InviteCodesView: View { Text("No invite codes found") .foregroundColor(.secondary) Button("Refresh") { - refreshInviteCodes() + Task { + await viewModel.refreshInviteCodes() + } } .padding() } @@ -49,7 +51,8 @@ struct InviteCodesView: View { .contextMenu { Button(role: .destructive) { Task { - await viewModel.pdsService.disableInviteCode(code.id) + // After disabling the code, refresh the list + await viewModel.disableInviteCode(code.id) } } label: { Label("Disable Code", systemImage: "xmark.circle") @@ -71,6 +74,10 @@ struct InviteCodesView: View { Toggle("Show Disabled Codes (\(disabledCodes.count))", isOn: $showDisabledCodes) .padding() .background(Color(.systemBackground)) + .onChange(of: showDisabledCodes) { _ in + // Refresh UI when toggle changes + viewModel.refreshUI() + } } } } @@ -81,6 +88,7 @@ struct InviteCodesView: View { isCreatingCode = true Task { await viewModel.pdsService.createInviteCode() + await viewModel.refreshInviteCodes() isCreatingCode = false } } label: { @@ -92,43 +100,18 @@ struct InviteCodesView: View { } .disabled(isCreatingCode) } - - ToolbarItem(placement: .navigationBarLeading) { - Button { - refreshInviteCodes() - } label: { - if isRefreshing { - ProgressView() - } else { - Label("Refresh", systemImage: "arrow.clockwise") - } - } - .disabled(isRefreshing) - } } .refreshable { - await viewModel.pdsService.fetchInviteCodes() + print("⏳ Pull-to-refresh: Fetching invite codes") + await viewModel.refreshInviteCodes() + print("✅ Pull-to-refresh completed") } } .task { - // Only fetch if we don't already have data - if viewModel.inviteCodes.isEmpty && !viewModel.isLoading { - await viewModel.pdsService.fetchInviteCodes() - } - } - .onAppear { - // Always attempt to refresh when view appears - if !viewModel.isLoading { - refreshInviteCodes() - } - } - } - - private func refreshInviteCodes() { - isRefreshing = true - Task { - await viewModel.pdsService.fetchInviteCodes() - isRefreshing = false + print("⏳ Task: Fetching invite codes") + // Always fetch on initial load + await viewModel.refreshInviteCodes() + print("✅ Task fetch completed") } } } diff --git a/PDSMan/Views/UserListView.swift b/PDSMan/Views/UserListView.swift index 29716a2..217dff4 100644 --- a/PDSMan/Views/UserListView.swift +++ b/PDSMan/Views/UserListView.swift @@ -22,7 +22,9 @@ struct UserListView: View { Text("No users found") .foregroundColor(.secondary) Button("Refresh") { - refreshUsers() + Task { + await viewModel.refreshUsers() + } } .padding() } @@ -70,22 +72,10 @@ struct UserListView: View { } } .navigationTitle("Users") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - refreshUsers() - } label: { - if isRefreshing { - ProgressView() - } else { - Label("Refresh", systemImage: "arrow.clockwise") - } - } - .disabled(isRefreshing) - } - } .refreshable { - await viewModel.pdsService.fetchUsers() + print("⏳ Pull-to-refresh: Fetching users") + await viewModel.refreshUsers() + print("✅ Pull-to-refresh completed") } .sheet(isPresented: $showingEditSheet) { if let user = editingUser { @@ -105,6 +95,7 @@ struct UserListView: View { Task { if await viewModel.pdsService.editUserHandle(userId: user.id, newHandle: newHandle) { showingEditSheet = false + await viewModel.refreshUsers() // Refresh to show updated handle } } } @@ -123,7 +114,7 @@ struct UserListView: View { if let user = selectedUser { Task { await viewModel.pdsService.suspendUser(userId: user.id, reason: suspensionReason) - await viewModel.pdsService.fetchUsers() // Refresh after suspension + await viewModel.refreshUsers() // Refresh after suspension } } } @@ -138,7 +129,7 @@ struct UserListView: View { if let user = selectedUser { Task { await viewModel.pdsService.reactivateUser(userId: user.id) - await viewModel.pdsService.fetchUsers() // Refresh after reactivation + await viewModel.refreshUsers() // Refresh after reactivation } } } @@ -149,24 +140,10 @@ struct UserListView: View { } } .task { - // Only fetch if we don't already have data - if viewModel.users.isEmpty && !viewModel.isLoading { - await viewModel.pdsService.fetchUsers() - } - } - .onAppear { - // Always attempt to refresh when view appears - if !viewModel.isLoading { - refreshUsers() - } - } - } - - private func refreshUsers() { - isRefreshing = true - Task { - await viewModel.pdsService.fetchUsers() - isRefreshing = false + print("⏳ Task: Fetching users") + // Always fetch on initial load + await viewModel.refreshUsers() + print("✅ Task fetch completed") } } }