krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

fb103fbabd9865f711f5052f63816b35fc259585

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-02T04:54:01Z

feat: expand user profile in Look Up
 Hutch/Models/User.swift                  |  9 ++++++
 Hutch/Views/Lookup/LookupView.swift      | 47 ++++++++++++++++++++++++-----
 Hutch/Views/Lookup/UserProfileView.swift | 52 +++++++++++++++++++++++++++++++-
 3 files changed, 100 insertions(+), 8 deletions(-)

diff --git a/Hutch/Models/User.swift b/Hutch/Models/User.swift
index a9350a3..625cfdc 100644
--- a/Hutch/Models/User.swift
+++ b/Hutch/Models/User.swift
@@ -3,8 +3,17 @@ import Foundation
 /// A Sourcehut user from meta.sr.ht.
 struct User: Decodable, Sendable {
     let id: Int
+    let created: String?
+    let updated: String?
     let username: String
     let canonicalName: String
     let email: String
+    let url: String?
+    let location: String?
+    let bio: String?
     let avatar: String?
+    let pronouns: String?
+    let userType: String?
+    let receivesPaidServices: Bool?
+    let suspensionNotice: String?
 }
diff --git a/Hutch/Views/Lookup/LookupView.swift b/Hutch/Views/Lookup/LookupView.swift
index 9f42d9e..413ea12 100644
--- a/Hutch/Views/Lookup/LookupView.swift
+++ b/Hutch/Views/Lookup/LookupView.swift
@@ -1,4 +1,7 @@
 import SwiftUI
+import os
+
+private let lookupLogger = Logger(subsystem: "net.cleberg.Hutch", category: "Lookup")
 
 enum LookupType: String, CaseIterable, Identifiable {
     case user = "User"
@@ -166,17 +169,47 @@ final class LookupViewModel {
         let query = """
         query userLookup($username: String!) {
             user: userByName(username: $username) {
-                id username canonicalName email avatar
+                id
+                created
+                updated
+                canonicalName
+                username
+                email
+                url
+                location
+                bio
+                avatar
+                pronouns
+                userType
             }
         }
         """
 
-        let result = try await client.execute(
-            service: .meta,
-            query: query,
-            variables: ["username": username],
-            responseType: Response.self
-        )
+        lookupLogger.debug("Looking up user profile for username: \(username, privacy: .public)")
+
+        let result: Response
+        do {
+            result = try await client.execute(
+                service: .meta,
+                query: query,
+                variables: ["username": username],
+                responseType: Response.self
+            )
+        } catch {
+            lookupLogger.error(
+                """
+                User lookup failed
+                username: \(username, privacy: .public)
+                query:
+                \(query, privacy: .public)
+                error:
+                \(String(describing: error), privacy: .public)
+                """
+            )
+            throw error
+        }
+
+        lookupLogger.debug("User lookup succeeded for username: \(username, privacy: .public)")
 
         return .user(result.user)
     }
diff --git a/Hutch/Views/Lookup/UserProfileView.swift b/Hutch/Views/Lookup/UserProfileView.swift
index 3fcbb2f..4c91244 100644
--- a/Hutch/Views/Lookup/UserProfileView.swift
+++ b/Hutch/Views/Lookup/UserProfileView.swift
@@ -3,7 +3,12 @@ import SwiftUI
 struct UserProfileView: View {
     let user: User
 
-    @Environment(AppState.self) private var appState
+    private static let iso8601Formatter: ISO8601DateFormatter = {
+        let formatter = ISO8601DateFormatter()
+        formatter.formatOptions = [.withInternetDateTime]
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        return formatter
+    }()
 
     var body: some View {
         List {
@@ -42,11 +47,56 @@ struct UserProfileView: View {
             Section {
                 LabeledContent("Username", value: user.username)
                 LabeledContent("Canonical Name", value: user.canonicalName)
+                if let userType = user.userType {
+                    LabeledContent("User Type", value: userType)
+                }
+                if let pronouns = user.pronouns {
+                    LabeledContent("Pronouns", value: pronouns)
+                }
+                if let suspensionNotice = user.suspensionNotice {
+                    LabeledContent("Suspension Notice", value: suspensionNotice)
+                }
+            }
+
+            Section {
                 LabeledContent("Email", value: user.email)
+                if let urlString = user.url, let url = URL(string: urlString) {
+                    LabeledContent("URL") {
+                        Link(urlString, destination: url)
+                    }
+                }
+                if let location = user.location {
+                    LabeledContent("Location", value: location)
+                }
+            }
+
+            if let bio = user.bio {
+                Section("Bio") {
+                    Text(bio)
+                }
+            }
+
+            if user.created != nil || user.updated != nil {
+                Section {
+                    if let created = user.created {
+                        LabeledContent("Joined", value: formattedTimestamp(created))
+                    }
+                    if let updated = user.updated {
+                        LabeledContent("Updated", value: formattedTimestamp(updated))
+                    }
+                }
             }
         }
         .listStyle(.insetGrouped)
         .navigationTitle(user.canonicalName)
         .navigationBarTitleDisplayMode(.inline)
     }
+
+    private func formattedTimestamp(_ value: String) -> String {
+        guard let date = Self.iso8601Formatter.date(from: value) else {
+            return value
+        }
+
+        return date.formatted(date: .abbreviated, time: .shortened)
+    }
 }