summaryrefslogtreecommitdiff
path: root/Hutch/App/AppState.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-05-14 12:41:55 -0500
committerChristian Cleberg <[email protected]>2026-05-14 12:41:55 -0500
commit0d61e05e98f6f020722ee4d43f024c1da348d2a9 (patch)
treeb388972b86cf83de380381c52c4390c3beafc69a /Hutch/App/AppState.swift
parentf28e5cee1643e3f99d7bf90476d1dcf9347229a0 (diff)
downloadhutch-3.4.0.tar.gz
hutch-3.4.0.tar.bz2
hutch-3.4.0.zip
feat: add Safari extension for sourcehut deep linksv3.4.0
Diffstat (limited to 'Hutch/App/AppState.swift')
-rw-r--r--Hutch/App/AppState.swift78
1 files changed, 76 insertions, 2 deletions
diff --git a/Hutch/App/AppState.swift b/Hutch/App/AppState.swift
index f91f1a4..a5500fe 100644
--- a/Hutch/App/AppState.swift
+++ b/Hutch/App/AppState.swift
@@ -2,6 +2,9 @@ import Foundation
import SwiftUI
import UIKit
import WebKit
+import os
+
+private let appStateDeepLinkLogger = Logger(subsystem: "net.cleberg.Hutch", category: "DeepLink")
/// Central application state shared across the view hierarchy.
@Observable
@@ -260,10 +263,11 @@ final class AppState {
/// Resolve a repository by owner and name for deep linking.
func resolveRepository(owner: String, name: String, service: SRHTService = .git) async throws -> RepositorySummary {
+ let normalizedOwner = Self.srhtUsername(from: owner)
let result = try await client.execute(
service: service,
query: Self.repoLookupQuery,
- variables: ["owner": owner, "name": name],
+ variables: ["owner": normalizedOwner, "name": name],
responseType: RepoLookupResponse.self
)
return result.user.repository
@@ -271,15 +275,39 @@ final class AppState {
/// Resolve a tracker by owner and name for deep linking.
func resolveTracker(owner: String, name: String) async throws -> TrackerSummary {
+ let normalizedOwner = Self.srhtUsername(from: owner)
let result = try await client.execute(
service: .todo,
query: Self.trackerLookupQuery,
- variables: ["owner": owner, "name": name],
+ variables: ["owner": normalizedOwner, "name": name],
responseType: TrackerLookupResponse.self
)
return result.user.tracker
}
+ func resolveMailingList(owner: String, name: String) async throws -> InboxMailingListReference {
+ let normalizedOwner = Self.srhtUsername(from: owner)
+ let result = try await client.execute(
+ service: .lists,
+ query: Self.mailingListLookupQuery,
+ variables: ["owner": normalizedOwner, "name": name],
+ responseType: MailingListLookupResponse.self
+ )
+ return result.user.mailingList
+ }
+
+ func resolveUser(username: String) async throws -> User {
+ let normalizedUsername = Self.srhtUsername(from: username)
+ appStateDeepLinkLogger.info("Resolving user. raw=\(username, privacy: .public), normalized=\(normalizedUsername, privacy: .public)")
+ let result = try await client.execute(
+ service: .meta,
+ query: Self.userLookupQuery,
+ variables: ["username": normalizedUsername],
+ responseType: UserLookupResponse.self
+ )
+ return result.user
+ }
+
func resolveProjectSource(_ source: Project.SourceRepo) async throws -> RepositorySummary {
try await resolveRepository(
owner: source.ownerUsername,
@@ -384,6 +412,11 @@ final class AppState {
return result.me
}
+ static func srhtUsername(from value: String) -> String {
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
+ return trimmed.hasPrefix("~") ? String(trimmed.dropFirst()) : trimmed
+ }
+
// MARK: - Deep link queries
private static let repoLookupQuery = """
@@ -425,6 +458,47 @@ final class AppState {
let tracker: TrackerSummary
}
+ private static let mailingListLookupQuery = """
+ query mailingListLookup($owner: String!, $name: String!) {
+ user(username: $owner) {
+ mailingList: list(name: $name) {
+ id rid name owner { canonicalName }
+ }
+ }
+ }
+ """
+
+ private struct MailingListLookupResponse: Decodable, Sendable {
+ let user: MailingListLookupUser
+ }
+
+ private struct MailingListLookupUser: Decodable, Sendable {
+ let mailingList: InboxMailingListReference
+ }
+
+ private static let userLookupQuery = """
+ query userLookup($username: String!) {
+ user: userByName(username: $username) {
+ id
+ created
+ updated
+ canonicalName
+ username
+ email
+ url
+ location
+ bio
+ avatar
+ pronouns
+ userType
+ }
+ }
+ """
+
+ private struct UserLookupResponse: Decodable, Sendable {
+ let user: User
+ }
+
private func addValidatedAccount(token: String, activateNewAccount: Bool) async throws {
let tempClient = SRHTClient(token: token)
let user = try await fetchMe(using: tempClient)