Commit fd4d8f49e0
Unsigned
Layout: unified · split
Hutch/Models/User.swift +9
| @@ -3,8 +3,17 @@ import Foundation | |||
| 3 | /// A Sourcehut user from meta.sr.ht. | 3 | /// A Sourcehut user from meta.sr.ht. |
| 4 | struct User: Decodable, Sendable { | 4 | struct User: Decodable, Sendable { |
| 5 | let id: Int | 5 | let id: Int |
| 6 | let created: String? | ||
| 7 | let updated: String? | ||
| 6 | let username: String | 8 | let username: String |
| 7 | let canonicalName: String | 9 | let canonicalName: String |
| 8 | let email: String | 10 | let email: String |
| 11 | let url: String? | ||
| 12 | let location: String? | ||
| 13 | let bio: String? | ||
| 9 | let avatar: String? | 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 | import SwiftUI | 1 | import SwiftUI |
| 2 | import os | ||
| 3 | |||
| 4 | private let lookupLogger = Logger(subsystem: "net.cleberg.Hutch", category: "Lookup") | ||
| 2 | 5 | ||
| 3 | enum LookupType: String, CaseIterable, Identifiable { | 6 | enum LookupType: String, CaseIterable, Identifiable { |
| 4 | case user = "User" | 7 | case user = "User" |
| @@ -166,17 +169,47 @@ final class LookupViewModel { | |||
| 166 | let query = """ | 169 | let query = """ |
| 167 | query userLookup($username: String!) { | 170 | query userLookup($username: String!) { |
| 168 | user: userByName(username: $username) { | 171 | user: userByName(username: $username) { |
| 169 | id username canonicalName email avatar | 172 | id |
| 173 | created | ||
| 174 | updated | ||
| 175 | canonicalName | ||
| 176 | username | ||
| 177 | |||
| 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( | 188 | lookupLogger.debug("Looking up user profile for username: \(username, privacy: .public)") |
| 175 | service: .meta, | 189 | |
| 176 | query: query, | 190 | let result: Response |
| 177 | variables: ["username": username], | 191 | do { |
| 178 | responseType: Response.self | 192 | result = try await client.execute( |
| 179 | ) | 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 | return .user(result.user) | 214 | return .user(result.user) |
| 182 | } | 215 | } |
Hutch/Views/Lookup/UserProfileView.swift +51 −1
| @@ -3,7 +3,12 @@ import SwiftUI | |||
| 3 | struct UserProfileView: View { | 3 | struct UserProfileView: View { |
| 4 | let user: User | 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 | var body: some View { | 13 | var body: some View { |
| 9 | List { | 14 | List { |
| @@ -42,11 +47,56 @@ struct UserProfileView: View { | |||
| 42 | Section { | 47 | Section { |
| 43 | LabeledContent("Username", value: user.username) | 48 | LabeledContent("Username", value: user.username) |
| 44 | LabeledContent("Canonical Name", value: user.canonicalName) | 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 | LabeledContent("Email", value: user.email) | 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 | .listStyle(.insetGrouped) | 90 | .listStyle(.insetGrouped) |
| 49 | .navigationTitle(user.canonicalName) | 91 | .navigationTitle(user.canonicalName) |
| 50 | .navigationBarTitleDisplayMode(.inline) | 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 | } |