Watch: review fixes for action errors that vanished, feed refreshes dropped or overwritten around a change, a fresh thread screen per route, errors cleared on sign out, notification actions only while paired, cache writes off the main thread and capped at 30 threads, Ask before acting and Load earlier races, two-letter search, C# headings, and strictly encoded query values

This commit is contained in:
gib committed 2026-11-02 15:29:15 -05:00
1 parent 99b300855e
commit 25be69fa30
14 files changed
+591 -94

No files matched your search

+14 -4
View File
@@ -197,6 +197,19 @@ struct GatewayClient: GatewayAPI {
error.code == .cancelled ? CancellationError() : GatewayError.transport(error.localizedDescription)
}
/// `path` under `baseURL`, then `query`. `appending(component:)` percent-encodes each segment,
/// slashes included. Query values are encoded strictly, so a `+`, `&` or `=` in a search or an
/// opaque history cursor reaches the gateway as sent.
static func url(_ baseURL: URL, _ path: [String], query: [URLQueryItem] = []) -> URL {
let url = path.reduce(baseURL) { $0.appending(component: $1) }
guard !query.isEmpty, var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
let unreserved = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
components.percentEncodedQueryItems = query.map {
URLQueryItem(name: $0.name, value: $0.value?.addingPercentEncoding(withAllowedCharacters: unreserved))
}
return components.url ?? url
}
private func call<Response: Decodable>(
_ method: String, _ path: [String], query: [URLQueryItem] = []
) async throws -> Response {
@@ -221,10 +234,7 @@ struct GatewayClient: GatewayAPI {
private func perform<Body: Encodable>(
_ method: String, _ path: [String], query: [URLQueryItem] = [], body: Body?
) async throws -> Data {
// appending(component:) percent-encodes each segment, slashes included.
var url = path.reduce(baseURL) { $0.appending(component: $1) }
if !query.isEmpty { url.append(queryItems: query) }
var request = URLRequest(url: url, timeoutInterval: 20)
var request = URLRequest(url: Self.url(baseURL, path, query: query), timeoutInterval: 20)
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Accept")
if let token {
+4 -1
View File
@@ -21,6 +21,8 @@ enum Route: Hashable, Sendable {
case search(query: String)
static let scheme = "wristcode"
/// The fewest characters the gateway searches for.
static let minimumSearch = 2
/// `wristcode://thread/<hostId>/<threadId>`, `wristcode://new`, `wristcode://recent`, `wristcode://snoozed`,
/// `wristcode://status`,
@@ -92,7 +94,8 @@ enum Route: Hashable, Sendable {
case "project" where parts.count == 2 && parts.allSatisfy { !$0.isEmpty }:
self = .projectThreads(hostId: parts[0], projectId: parts[1])
case "search":
guard let query = components.queryItems?.first(where: { $0.name == "q" })?.value, !query.isEmpty else { return nil }
guard let query = components.queryItems?.first(where: { $0.name == "q" })?.value, query.count >= Self.minimumSearch
else { return nil }
self = .search(query: query)
default:
return nil