Commit fd4d8f49e0
fd4d8f49e05054c05722cb16da555943d4007736
parent: c619fe4a96
Unsigned
cmc <hello@cleberg.net> · 2026-04-02 04:54 UTC
feat: expand user profile in Look Up
Layout: unified · split
Hutch/Models/User.swift
+9
| @@ -3,8 +3,17 @@ import Foundation |
| 3 | 3 | /// A Sourcehut user from meta.sr.ht. |
| 4 | 4 | struct User: Decodable, Sendable { |
| 5 | 5 | let id: Int |
| 6 | let created: String? |
| 7 | let updated: String? |
| 6 | 8 | let username: String |
| 7 | 9 | let canonicalName: String |
| 8 | 10 | let email: String |
| 11 | let url: String? |
| 12 | let location: String? |
| 13 | let bio: String? |
| 9 | 14 | let avatar: String? |
| 15 | let pronouns: String? |
| 16 | let userType: String? |
| 17 | let receivesPaidServices: Bool? |
| 18 | let suspensionNotice: String? |
| 10 | 19 | } |
Hutch/Views/Lookup/LookupView.swift
+40 −7
| @@ -1,4 +1,7 @@ |
| 1 | 1 | import SwiftUI |
| 2 | import os |
| 3 | |
| 4 | private let lookupLogger = Logger(subsystem: "net.cleberg.Hutch", category: "Lookup") |
| 2 | 5 | |
| 3 | 6 | enum LookupType: String, CaseIterable, Identifiable { |
| 4 | 7 | case user = "User" |
| @@ -166,17 +169,47 @@ final class LookupViewModel { |
| 166 | 169 | let query = """ |
| 167 | 170 | query userLookup($username: String!) { |
| 168 | 171 | user: userByName(username: $username) { |
| 169 | | id username canonicalName email avatar |
| 172 | id |
| 173 | created |
| 174 | updated |
| 175 | canonicalName |
| 176 | username |
| 177 | email |
| 178 | url |
| 179 | location |
| 180 | bio |
| 181 | avatar |
| 182 | pronouns |
| 183 | userType |
| 170 | 184 | } |
| 171 | 185 | } |
| 172 | 186 | """ |
| 173 | 187 | |
| 174 | | let result = try await client.execute( |
| 175 | | service: .meta, |
| 176 | | query: query, |
| 177 | | variables: ["username": username], |
| 178 | | responseType: Response.self |
| 179 | | ) |
| 188 | lookupLogger.debug("Looking up user profile for username: \(username, privacy: .public)") |
| 189 | |
| 190 | let result: Response |
| 191 | do { |
| 192 | result = try await client.execute( |
| 193 | service: .meta, |
| 194 | query: query, |
| 195 | variables: ["username": username], |
| 196 | responseType: Response.self |
| 197 | ) |
| 198 | } catch { |
| 199 | lookupLogger.error( |
| 200 | """ |
| 201 | User lookup failed |
| 202 | username: \(username, privacy: .public) |
| 203 | query: |
| 204 | \(query, privacy: .public) |
| 205 | error: |
| 206 | \(String(describing: error), privacy: .public) |
| 207 | """ |
| 208 | ) |
| 209 | throw error |
| 210 | } |
| 211 | |
| 212 | lookupLogger.debug("User lookup succeeded for username: \(username, privacy: .public)") |
| 180 | 213 | |
| 181 | 214 | return .user(result.user) |
| 182 | 215 | } |
Hutch/Views/Lookup/UserProfileView.swift
+51 −1
| @@ -3,7 +3,12 @@ import SwiftUI |
| 3 | 3 | struct UserProfileView: View { |
| 4 | 4 | let user: User |
| 5 | 5 | |
| 6 | | @Environment(AppState.self) private var appState |
| 6 | private static let iso8601Formatter: ISO8601DateFormatter = { |
| 7 | let formatter = ISO8601DateFormatter() |
| 8 | formatter.formatOptions = [.withInternetDateTime] |
| 9 | formatter.timeZone = TimeZone(secondsFromGMT: 0) |
| 10 | return formatter |
| 11 | }() |
| 7 | 12 | |
| 8 | 13 | var body: some View { |
| 9 | 14 | List { |
| @@ -42,11 +47,56 @@ struct UserProfileView: View { |
| 42 | 47 | Section { |
| 43 | 48 | LabeledContent("Username", value: user.username) |
| 44 | 49 | LabeledContent("Canonical Name", value: user.canonicalName) |
| 50 | if let userType = user.userType { |
| 51 | LabeledContent("User Type", value: userType) |
| 52 | } |
| 53 | if let pronouns = user.pronouns { |
| 54 | LabeledContent("Pronouns", value: pronouns) |
| 55 | } |
| 56 | if let suspensionNotice = user.suspensionNotice { |
| 57 | LabeledContent("Suspension Notice", value: suspensionNotice) |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | Section { |
| 45 | 62 | LabeledContent("Email", value: user.email) |
| 63 | if let urlString = user.url, let url = URL(string: urlString) { |
| 64 | LabeledContent("URL") { |
| 65 | Link(urlString, destination: url) |
| 66 | } |
| 67 | } |
| 68 | if let location = user.location { |
| 69 | LabeledContent("Location", value: location) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | if let bio = user.bio { |
| 74 | Section("Bio") { |
| 75 | Text(bio) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | if user.created != nil || user.updated != nil { |
| 80 | Section { |
| 81 | if let created = user.created { |
| 82 | LabeledContent("Joined", value: formattedTimestamp(created)) |
| 83 | } |
| 84 | if let updated = user.updated { |
| 85 | LabeledContent("Updated", value: formattedTimestamp(updated)) |
| 86 | } |
| 87 | } |
| 46 | 88 | } |
| 47 | 89 | } |
| 48 | 90 | .listStyle(.insetGrouped) |
| 49 | 91 | .navigationTitle(user.canonicalName) |
| 50 | 92 | .navigationBarTitleDisplayMode(.inline) |
| 51 | 93 | } |
| 94 | |
| 95 | private func formattedTimestamp(_ value: String) -> String { |
| 96 | guard let date = Self.iso8601Formatter.date(from: value) else { |
| 97 | return value |
| 98 | } |
| 99 | |
| 100 | return date.formatted(date: .abbreviated, time: .shortened) |
| 101 | } |
| 52 | 102 | } |